Skip to content

Commit 5ff91b6

Browse files
authored
Merge pull request #694 from DirectoryTree/FEATURE-640
Feature 640 - Add ability to morph models into other models based on object classes
2 parents 52840f3 + 886ee7b commit 5ff91b6

File tree

6 files changed

+129
-57
lines changed

6 files changed

+129
-57
lines changed

src/Connection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public function __construct(DomainConfiguration|array $config = [], LdapInterfac
7575
{
7676
$this->setConfiguration($config);
7777

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

8080
$this->failed = function () {
8181
$this->dispatch(new Events\ConnectionFailed($this));

src/Models/Model.php

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use LdapRecord\Query\Builder as BaseBuilder;
1515
use LdapRecord\Query\Model\Builder;
1616
use LdapRecord\Support\Arr;
17+
use RuntimeException;
1718
use Stringable;
1819
use UnexpectedValueException;
1920

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

7273
/**
73-
* The attribute key that contains the models object GUID.
74+
* The attribute key containing the models object GUID.
7475
*/
7576
protected string $guidKey = 'objectguid';
7677

7778
/**
78-
* The array of booted models.
79+
* The array of the model's modifications.
7980
*/
80-
protected static array $booted = [];
81+
protected array $modifications = [];
8182

8283
/**
83-
* Contains the models modifications.
84+
* The array of booted models.
8485
*/
85-
protected array $modifications = [];
86+
protected static array $booted = [];
8687

8788
/**
8889
* The array of global scopes on the model.
8990
*/
9091
protected static array $globalScopes = [];
9192

93+
/**
94+
* The morph model cache containing object classes and their corresponding models.
95+
*/
96+
protected static array $morphCache = [];
97+
9298
/**
9399
* Constructor.
94100
*/
@@ -120,7 +126,7 @@ protected static function boot(): void
120126
}
121127

122128
/**
123-
* Clear the list of booted models so they will be re-booted.
129+
* Clear the list of booted models, so they will be re-booted.
124130
*/
125131
public static function clearBootedModels(): void
126132
{
@@ -568,6 +574,70 @@ public function hydrate(array $records): Collection
568574
});
569575
}
570576

577+
/**
578+
* Morph the model into a one of matching models using their object classes.
579+
*/
580+
public function morphInto(array $models, callable $resolver = null): Model
581+
{
582+
if (class_exists($model = $this->determineMorphModel($this, $models, $resolver))) {
583+
return $this->convert(new $model);
584+
}
585+
586+
return $this;
587+
}
588+
589+
/**
590+
* Morph the model into a one of matching models or throw an exception.
591+
*/
592+
public function morphIntoOrFail(array $models, callable $resolver = null): Model
593+
{
594+
$model = $this->morphInto($models, $resolver);
595+
596+
if ($model instanceof $this) {
597+
throw new RuntimeException(
598+
'The model could not be morphed into any of the given models.'
599+
);
600+
}
601+
602+
return $model;
603+
}
604+
605+
/**
606+
* Determine the model to morph into from the given models.
607+
*
608+
* @return class-string|bool
609+
*/
610+
protected function determineMorphModel(Model $model, array $models, callable $resolver = null): string|bool
611+
{
612+
$morphModelMap = [];
613+
614+
foreach ($models as $modelClass) {
615+
$morphModelMap[$modelClass] = static::$morphCache[$modelClass] ??= $this->normalizeObjectClasses(
616+
$modelClass::$objectClasses
617+
);
618+
}
619+
620+
$objectClasses = $this->normalizeObjectClasses(
621+
$model->getObjectClasses()
622+
);
623+
624+
$resolver ??= function (array $objectClasses, array $morphModelMap) {
625+
return array_search($objectClasses, $morphModelMap);
626+
};
627+
628+
return $resolver($objectClasses, $morphModelMap);
629+
}
630+
631+
/**
632+
* Sort and normalize the object classes.
633+
*/
634+
protected function normalizeObjectClasses(array $classes): array
635+
{
636+
sort($classes);
637+
638+
return array_map('strtolower', $classes);
639+
}
640+
571641
/**
572642
* Converts the current model into the given model.
573643
*/

src/Models/Relations/Relation.php

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ protected function getParentForeignValue(): ?string
275275
protected function getForeignValueFromModel(Model $model): ?string
276276
{
277277
return $this->foreignKeyIsDistinguishedName()
278-
? $model->getDn()
279-
: $this->getFirstAttributeValue($model, $this->foreignKey);
278+
? $model->getDn()
279+
: $this->getFirstAttributeValue($model, $this->foreignKey);
280280
}
281281

282282
/**
@@ -292,19 +292,9 @@ protected function getFirstAttributeValue(Model $model, string $attribute): mixe
292292
*/
293293
protected function transformResults(Collection $results): Collection
294294
{
295-
$relationMap = [];
296-
297-
foreach ($this->related as $relation) {
298-
$relationMap[$relation] = $this->normalizeObjectClasses(
299-
$relation::$objectClasses
300-
);
301-
}
302-
303-
return $results->transform(fn (Model $entry) => (
304-
class_exists($model = $this->determineModelFromRelated($entry, $relationMap))
305-
? $entry->convert(new $model)
306-
: $entry
307-
));
295+
return $results->transform(
296+
fn (Model $entry) => $entry->morphInto($this->related, static::$modelResolver)
297+
);
308298
}
309299

310300
/**
@@ -314,35 +304,4 @@ protected function foreignKeyIsDistinguishedName(): bool
314304
{
315305
return in_array($this->foreignKey, ['dn', 'distinguishedname']);
316306
}
317-
318-
/**
319-
* Determine the model from the given relation map.
320-
*
321-
* @return class-string|bool
322-
*/
323-
protected function determineModelFromRelated(Model $model, array $relationMap): string|bool
324-
{
325-
// We must normalize all the related models object class
326-
// names to the same case so we are able to properly
327-
// determine the owning model from search results.
328-
$modelObjectClasses = $this->normalizeObjectClasses(
329-
$model->getObjectClasses()
330-
);
331-
332-
$resolver = static::$modelResolver ?? function (array $modelObjectClasses, array $relationMap) {
333-
return array_search($modelObjectClasses, $relationMap);
334-
};
335-
336-
return call_user_func($resolver, $modelObjectClasses, $relationMap, $model);
337-
}
338-
339-
/**
340-
* Sort and normalize the object classes.
341-
*/
342-
protected function normalizeObjectClasses(array $classes): array
343-
{
344-
sort($classes);
345-
346-
return array_map('strtolower', $classes);
347-
}
348307
}

src/Query/ObjectNotFoundException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ObjectNotFoundException extends LdapRecordException
2121
*/
2222
public static function forQuery(string $query, string $baseDn = null): static
2323
{
24-
return (new static())->setQuery($query, $baseDn);
24+
return (new static)->setQuery($query, $baseDn);
2525
}
2626

2727
/**

tests/Unit/Models/ModelHasManyTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function test_chunk()
9393
$query->shouldReceive('getSelects')->once()->withNoArgs()->andReturn(['*']);
9494
$query->shouldReceive('whereRaw')->once()->with('member', '=', 'foo')->andReturnSelf();
9595
$query->shouldReceive('chunk')->once()->with(1000, m::on(function ($callback) {
96-
$related = m::mock(ModelHasManyStub::class);
96+
$related = m::mock(ModelHasManyStub::class)->makePartial();
9797

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

125125
$related->shouldReceive('getDn')->andReturn('bar');
126126
$related->shouldReceive('convert')->once()->andReturnSelf();
@@ -210,7 +210,7 @@ public function test_detaching_all()
210210
$parent->shouldReceive('getDn')->andReturn('foo');
211211
$parent->shouldReceive('newCollection')->once()->andReturn(new Collection());
212212

213-
$related = m::mock(Entry::class);
213+
$related = m::mock(Entry::class)->makePartial();
214214
$related->shouldReceive('getObjectClasses')->once()->andReturn([]);
215215
$related->shouldReceive('convert')->once()->andReturnSelf();
216216
$related->shouldReceive('removeAttribute')->once()->with('member', 'foo')->andReturnTrue();

tests/Unit/Models/ModelTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
use LdapRecord\Connection;
88
use LdapRecord\Container;
99
use LdapRecord\ContainerException;
10+
use LdapRecord\Models\ActiveDirectory\Group;
11+
use LdapRecord\Models\ActiveDirectory\User;
1012
use LdapRecord\Models\Attributes\Timestamp;
1113
use LdapRecord\Models\BatchModification;
1214
use LdapRecord\Models\Entry;
1315
use LdapRecord\Models\Model;
1416
use LdapRecord\Testing\DirectoryFake;
1517
use LdapRecord\Testing\LdapFake;
1618
use LdapRecord\Tests\TestCase;
19+
use RuntimeException;
1720

1821
class ModelTest extends TestCase
1922
{
@@ -795,6 +798,46 @@ public function test_setting_dn_attributes_set_distinguished_name_on_model()
795798
$this->assertEquals('foo', $model->getDn());
796799
$this->assertEquals(['foo'], $model->getAttributes()['distinguishedname']);
797800
}
801+
802+
public function test_morph_into()
803+
{
804+
$entry = new Entry([
805+
'objectclass' => User::$objectClasses,
806+
]);
807+
808+
$this->assertInstanceOf(Entry::class, $entry->morphInto([Group::class]));
809+
$this->assertInstanceOf(User::class, $entry->morphInto([Group::class, User::class]));
810+
}
811+
812+
public function test_morph_into_or_fail()
813+
{
814+
$entry = new Entry([
815+
'objectclass' => User::$objectClasses,
816+
]);
817+
818+
$this->assertInstanceOf(User::class, $entry->morphInto([Group::class, User::class]));
819+
820+
$this->expectException(RuntimeException::class);
821+
$this->expectExceptionMessage('The model could not be morphed into any of the given models.');
822+
823+
$entry->morphIntoOrFail([Group::class]);
824+
}
825+
826+
public function test_morph_into_with_custom_resolver_callback()
827+
{
828+
$entry = new Entry([
829+
'objectclass' => User::$objectClasses,
830+
]);
831+
832+
$group = $entry->morphInto([Group::class, User::class], function (array $objectClasses, array $models) use ($entry) {
833+
$this->assertEqualsCanonicalizing($entry->getObjectClasses(), $objectClasses);
834+
$this->assertEquals([Group::class, User::class], array_keys($models));
835+
836+
return Group::class;
837+
});
838+
839+
$this->assertInstanceOf(Group::class, $group);
840+
}
798841
}
799842

800843
class ModelCreateTestStub extends Model

0 commit comments

Comments
 (0)