Skip to content

Commit

Permalink
Resolve #9: Implement repositories (#10)
Browse files Browse the repository at this point in the history
Implements the repository design pattern which differs slightly from Doctrine's design. The worker and serializer create objects from the Ldap responses and keep track of changes which can be committed back to the Ldap server. The custom repository services for each model provide a simple interface for querying and persisting entries. 
Commits:
* Add query building in repositories
* Introduce a first complete workflow
* Remove the ClassMetadataInterface
* Implement rollbacks
* Add attribute type options
* Add Symfony connection
* Fix CS
* Fix tests
  • Loading branch information
indecim authored Jan 31, 2021
1 parent f2f31a2 commit 2e35453
Show file tree
Hide file tree
Showing 82 changed files with 4,037 additions and 586 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:

- name: Install Global Dependencies
run: |
composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-master
composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
- name: Install dependencies
run: |
composer config minimum-stability stable
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
"KAGOnlineTeam\\LdapBundle\\Tests\\": "tests/"
}
}
}
}
105 changes: 105 additions & 0 deletions src/AbstractRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace KAGOnlineTeam\LdapBundle;

use KAGOnlineTeam\LdapBundle\Query\Builder;
use KAGOnlineTeam\LdapBundle\Query\Options;
use KAGOnlineTeam\LdapBundle\Request\QueryRequest;

abstract class AbstractRepository implements RepositoryInterface
{
private $manager;
private $metadata;
private $worker;

public function __construct(ManagerInterface $manager, string $class, Worker $worker = null)
{
$this->manager = $manager;
$this->metadata = $manager->getMetadata($class);

$this->worker = $worker ?: new Worker($this->metadata);
}

/**
* {@inheritdoc}
*/
public function getClass(): string
{
return $this->metadata->getClass();
}

/**
* Finds an entry by a distinguished name.
*
* @param string $dn The distinguished name
*
* @return object|null An entry object if found
*/
public function find(string $dn): ?object
{
$qb = $this->createQueryBuilder()
->in($dn)
->scope(Options::SCOPE_BASE)
->make();

$entries = iterator_to_array($this->execute($qb));

return 0 === \count($entries) ? null : $entries[0];
}

/**
* Finds all entries.
*
* @param string $dn The distinguished name
*/
public function findAll(): iterable
{
$query = $this->createQueryBuilder()
->in(Options::BASE_DN)
->scope(Options::SCOPE_BASE)
->make();

return $this->execute($query);
}

/**
* {@inheritdoc}
*/
public function persist(object $entry): void
{
$this->worker->mark($entry, Worker::MARK_PERSISTENCE);
}

/**
* {@inheritdoc}
*/
public function remove(object $entry): void
{
$this->worker->mark($entry, Worker::MARK_REMOVAL);
}

/**
* {@inheritdoc}
*/
public function commit(): void
{
$this->manager->update($this->worker->createRequests());
}

/**
* @return Builder A fresh query builder instance
*/
protected function createQueryBuilder(): Builder
{
return new Builder($this->manager->getBaseDn(), $this->metadata);
}

protected function execute(QueryRequest $request): iterable
{
$this->worker->update(
$this->manager->query($request)
);

return $this->worker->fetchLatest();
}
}
8 changes: 7 additions & 1 deletion src/Annotation/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
* @Annotation
* @Target({"PROPERTY"})
* @Attributes(
* @Attribute("description", type="string", required=true)
* @Attribute("description", type="string", required=true),
* @Attribute("type", type="string", required=true),
* )
*/
class Attribute
{
public $description;

/**
* @Enum({"array", "scalar", "multivalue"})
*/
public $type;
}
7 changes: 7 additions & 0 deletions src/Annotation/DistinguishedName.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
*
* @Annotation
* @Target({"PROPERTY"})
* @Attributes(
* @Attribute("type", type="string", required=true),
* )
*/
class DistinguishedName
{
/**
* @Enum({"string", "object"})
*/
public $type;
}
196 changes: 196 additions & 0 deletions src/Attribute/DistinguishedName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

namespace KAGOnlineTeam\LdapBundle\Attribute;

/**
* An OOP implementation of a distinguished name of a LDAP entry.
*
* @author Jan Flaßkamp
*/
class DistinguishedName
{
/**
* Holds all information of the original DN string in an array.
*
* The array consists of zero or more RDNs which are arrays by themselves.
* To support multivalue RDNs a single RDN is an array of name-value pairs.
* E.g. the dn "cn=John+employeeNumber=1,ou=users,ou=system" results in:
* [
* [["cn", "John"], ["employeeNumber", "1"]],
* [["ou", "users"]],
* [["ou", "system"]]
* ]
*/
protected $rdns = [];

public function __construct(array $rdns = [])
{
$this->rdns = $rdns;
}

/**
* Creates a new DN object from a DN string.
*/
public static function deserialize(string $dn): self
{
$rdns = ldap_explode_dn($dn, 0);

if (\is_array($rdns) && \array_key_exists('count', $rdns)) {
unset($rdns['count']);

foreach ($rdns as $key => $rdn) {
$rdns[$key] = [];
// Handle multivalued RDNs.
foreach (explode('+', $rdn) as $value) {
$pos = strpos($value, '=');
if (false === $pos) {
throw new \InvalidArgumentException(sprintf('Expected "=" in RDN ("%s").', $value));
}

$name = substr($value, 0, $pos);

// Unescape characters.
$value = preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
return \chr(hexdec($matches[1]));
}, substr($value, $pos + 1));

$rdns[$key][] = [$name, $value];
}
}
}

return new self($rdns);
}

/**
* Returns the string representation of the DN object.
*
* @return string The DN string
*/
public function serialize(): string
{
$rdns = [];

foreach ($this->rdns as $rdn) {
$rdnPairs = [];
foreach ($rdn as $pair) {
$value = ldap_escape($pair[1], '', \LDAP_ESCAPE_DN);
if (!empty($value) && ' ' === $value[0]) {
$value = '\\20'.substr($value, 1);
}
if (!empty($value) && ' ' === $value[\strlen($value) - 1]) {
$value = substr($value, 0, -1).'\\20';
}
$value = str_replace("\r", '\0d', $value);

$rdnPairs[] = $pair[0].'='.$value;
}

$rdns[] = implode('+', $rdnPairs);
}

return implode(',', $rdns);
}

public function __toString()
{
return $this->serialize();
}

/**
* Returns all RDNs.
*
* @return array An array of RDNs
*/
public function all(): array
{
return $this->rdns;
}

/**
* Returns the number of RDNs the DN contains. Multivalued RDNs will be
* counted as one.
*/
public function count(): int
{
return \count($this->rdns);
}

/**
* Returns the first RDN as a string.
*/
public function getRdn(): string
{
if (empty($this->rdns)) {
throw new LogicException('The DN has zero RDNs.');
}

// Get the RDN through the serialization process of an object.
$rdn = (new self($this->rdns[0]))
->serialize();

return $rdn;
}

/**
* Returns a new DistinguishedName object of the parent entry.
*
* @throws LogicException If there is no parent entry
*
* @return DistinguishedName A new DN object of the parent entry
*/
public function getParent(): self
{
if (empty($this->rdns)) {
throw new LogicException('Cannot get the parent DN of the root entry.');
}

return new self(\array_slice($this->rdns, 1));
}

/**
* Changes into the DN of the parent.
*
* @return $this
*/
public function removeRdn(): self
{
if (!empty($this->rdns)) {
$this->rdns = \array_slice($this->rdns, 1);
}

return $this;
}

/**
* Extends the distinguished name by adding a new RDN.
*
* @param array $pairs
*
* @return $this
*/
public function addRdn(...$pairs): self
{
if (\count($pairs) < 1) {
throw new \InvalidArgumentException('Expected at least one name-value pair.');
}

foreach ($pairs as $pair) {
if (!\is_array($pair)) {
throw new \InvalidArgumentException('A name-value pair must be of type array.');
}

if (2 !== \count($pair)) {
throw new \InvalidArgumentException('A name-value pair must consist of exactly two elements.');
}

if (!\is_string($pair[0]) || !\is_string($pair[1])) {
throw new \InvalidArgumentException('The name and value must be of type string.');
}
}

array_unshift($this->rdns, $pairs);

return $this;
}
}
Loading

0 comments on commit 2e35453

Please sign in to comment.