Skip to content

Commit

Permalink
Merge pull request #694 from DirectoryTree/FEATURE-640
Browse files Browse the repository at this point in the history
Feature 640 - Add ability to morph models into other models based on object classes
  • Loading branch information
stevebauman authored Feb 4, 2024
2 parents 52840f3 + 886ee7b commit 5ff91b6
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 57 deletions.
2 changes: 1 addition & 1 deletion src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function __construct(DomainConfiguration|array $config = [], LdapInterfac
{
$this->setConfiguration($config);

$this->setLdapConnection($ldap ?? new Ldap());
$this->setLdapConnection($ldap ?? new Ldap);

$this->failed = function () {
$this->dispatch(new Events\ConnectionFailed($this));
Expand Down
82 changes: 76 additions & 6 deletions src/Models/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use LdapRecord\Query\Builder as BaseBuilder;
use LdapRecord\Query\Model\Builder;
use LdapRecord\Support\Arr;
use RuntimeException;
use Stringable;
use UnexpectedValueException;

Expand Down Expand Up @@ -70,25 +71,30 @@ abstract class Model implements ArrayAccess, Arrayable, JsonSerializable, String
protected ?string $connection = null;

/**
* The attribute key that contains the models object GUID.
* The attribute key containing the models object GUID.
*/
protected string $guidKey = 'objectguid';

/**
* The array of booted models.
* The array of the model's modifications.
*/
protected static array $booted = [];
protected array $modifications = [];

/**
* Contains the models modifications.
* The array of booted models.
*/
protected array $modifications = [];
protected static array $booted = [];

/**
* The array of global scopes on the model.
*/
protected static array $globalScopes = [];

/**
* The morph model cache containing object classes and their corresponding models.
*/
protected static array $morphCache = [];

/**
* Constructor.
*/
Expand Down Expand Up @@ -120,7 +126,7 @@ protected static function boot(): void
}

/**
* Clear the list of booted models so they will be re-booted.
* Clear the list of booted models, so they will be re-booted.
*/
public static function clearBootedModels(): void
{
Expand Down Expand Up @@ -568,6 +574,70 @@ public function hydrate(array $records): Collection
});
}

/**
* Morph the model into a one of matching models using their object classes.
*/
public function morphInto(array $models, callable $resolver = null): Model
{
if (class_exists($model = $this->determineMorphModel($this, $models, $resolver))) {
return $this->convert(new $model);
}

return $this;
}

/**
* Morph the model into a one of matching models or throw an exception.
*/
public function morphIntoOrFail(array $models, callable $resolver = null): Model
{
$model = $this->morphInto($models, $resolver);

if ($model instanceof $this) {
throw new RuntimeException(
'The model could not be morphed into any of the given models.'
);
}

return $model;
}

/**
* Determine the model to morph into from the given models.
*
* @return class-string|bool
*/
protected function determineMorphModel(Model $model, array $models, callable $resolver = null): string|bool
{
$morphModelMap = [];

foreach ($models as $modelClass) {
$morphModelMap[$modelClass] = static::$morphCache[$modelClass] ??= $this->normalizeObjectClasses(
$modelClass::$objectClasses
);
}

$objectClasses = $this->normalizeObjectClasses(
$model->getObjectClasses()
);

$resolver ??= function (array $objectClasses, array $morphModelMap) {
return array_search($objectClasses, $morphModelMap);
};

return $resolver($objectClasses, $morphModelMap);
}

/**
* Sort and normalize the object classes.
*/
protected function normalizeObjectClasses(array $classes): array
{
sort($classes);

return array_map('strtolower', $classes);
}

/**
* Converts the current model into the given model.
*/
Expand Down
51 changes: 5 additions & 46 deletions src/Models/Relations/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ protected function getParentForeignValue(): ?string
protected function getForeignValueFromModel(Model $model): ?string
{
return $this->foreignKeyIsDistinguishedName()
? $model->getDn()
: $this->getFirstAttributeValue($model, $this->foreignKey);
? $model->getDn()
: $this->getFirstAttributeValue($model, $this->foreignKey);
}

/**
Expand All @@ -292,19 +292,9 @@ protected function getFirstAttributeValue(Model $model, string $attribute): mixe
*/
protected function transformResults(Collection $results): Collection
{
$relationMap = [];

foreach ($this->related as $relation) {
$relationMap[$relation] = $this->normalizeObjectClasses(
$relation::$objectClasses
);
}

return $results->transform(fn (Model $entry) => (
class_exists($model = $this->determineModelFromRelated($entry, $relationMap))
? $entry->convert(new $model)
: $entry
));
return $results->transform(
fn (Model $entry) => $entry->morphInto($this->related, static::$modelResolver)
);
}

/**
Expand All @@ -314,35 +304,4 @@ protected function foreignKeyIsDistinguishedName(): bool
{
return in_array($this->foreignKey, ['dn', 'distinguishedname']);
}

/**
* Determine the model from the given relation map.
*
* @return class-string|bool
*/
protected function determineModelFromRelated(Model $model, array $relationMap): string|bool
{
// We must normalize all the related models object class
// names to the same case so we are able to properly
// determine the owning model from search results.
$modelObjectClasses = $this->normalizeObjectClasses(
$model->getObjectClasses()
);

$resolver = static::$modelResolver ?? function (array $modelObjectClasses, array $relationMap) {
return array_search($modelObjectClasses, $relationMap);
};

return call_user_func($resolver, $modelObjectClasses, $relationMap, $model);
}

/**
* Sort and normalize the object classes.
*/
protected function normalizeObjectClasses(array $classes): array
{
sort($classes);

return array_map('strtolower', $classes);
}
}
2 changes: 1 addition & 1 deletion src/Query/ObjectNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ObjectNotFoundException extends LdapRecordException
*/
public static function forQuery(string $query, string $baseDn = null): static
{
return (new static())->setQuery($query, $baseDn);
return (new static)->setQuery($query, $baseDn);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/Models/ModelHasManyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function test_chunk()
$query->shouldReceive('getSelects')->once()->withNoArgs()->andReturn(['*']);
$query->shouldReceive('whereRaw')->once()->with('member', '=', 'foo')->andReturnSelf();
$query->shouldReceive('chunk')->once()->with(1000, m::on(function ($callback) {
$related = m::mock(ModelHasManyStub::class);
$related = m::mock(ModelHasManyStub::class)->makePartial();

$related->shouldReceive('getDn')->andReturn('bar');
$related->shouldReceive('convert')->once()->andReturnSelf();
Expand All @@ -120,7 +120,7 @@ public function test_recursive_chunk()
$query->shouldReceive('getSelects')->once()->withNoArgs()->andReturn(['*']);
$query->shouldReceive('whereRaw')->once()->with('member', '=', 'foo')->andReturnSelf();
$query->shouldReceive('chunk')->once()->with(1000, m::on(function ($callback) {
$related = m::mock(ModelHasManyStub::class);
$related = m::mock(ModelHasManyStub::class)->makePartial();

$related->shouldReceive('getDn')->andReturn('bar');
$related->shouldReceive('convert')->once()->andReturnSelf();
Expand Down Expand Up @@ -210,7 +210,7 @@ public function test_detaching_all()
$parent->shouldReceive('getDn')->andReturn('foo');
$parent->shouldReceive('newCollection')->once()->andReturn(new Collection());

$related = m::mock(Entry::class);
$related = m::mock(Entry::class)->makePartial();
$related->shouldReceive('getObjectClasses')->once()->andReturn([]);
$related->shouldReceive('convert')->once()->andReturnSelf();
$related->shouldReceive('removeAttribute')->once()->with('member', 'foo')->andReturnTrue();
Expand Down
43 changes: 43 additions & 0 deletions tests/Unit/Models/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
use LdapRecord\Connection;
use LdapRecord\Container;
use LdapRecord\ContainerException;
use LdapRecord\Models\ActiveDirectory\Group;
use LdapRecord\Models\ActiveDirectory\User;
use LdapRecord\Models\Attributes\Timestamp;
use LdapRecord\Models\BatchModification;
use LdapRecord\Models\Entry;
use LdapRecord\Models\Model;
use LdapRecord\Testing\DirectoryFake;
use LdapRecord\Testing\LdapFake;
use LdapRecord\Tests\TestCase;
use RuntimeException;

class ModelTest extends TestCase
{
Expand Down Expand Up @@ -795,6 +798,46 @@ public function test_setting_dn_attributes_set_distinguished_name_on_model()
$this->assertEquals('foo', $model->getDn());
$this->assertEquals(['foo'], $model->getAttributes()['distinguishedname']);
}

public function test_morph_into()
{
$entry = new Entry([
'objectclass' => User::$objectClasses,
]);

$this->assertInstanceOf(Entry::class, $entry->morphInto([Group::class]));
$this->assertInstanceOf(User::class, $entry->morphInto([Group::class, User::class]));
}

public function test_morph_into_or_fail()
{
$entry = new Entry([
'objectclass' => User::$objectClasses,
]);

$this->assertInstanceOf(User::class, $entry->morphInto([Group::class, User::class]));

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('The model could not be morphed into any of the given models.');

$entry->morphIntoOrFail([Group::class]);
}

public function test_morph_into_with_custom_resolver_callback()
{
$entry = new Entry([
'objectclass' => User::$objectClasses,
]);

$group = $entry->morphInto([Group::class, User::class], function (array $objectClasses, array $models) use ($entry) {
$this->assertEqualsCanonicalizing($entry->getObjectClasses(), $objectClasses);
$this->assertEquals([Group::class, User::class], array_keys($models));

return Group::class;
});

$this->assertInstanceOf(Group::class, $group);
}
}

class ModelCreateTestStub extends Model
Expand Down

0 comments on commit 5ff91b6

Please sign in to comment.