Skip to content

Commit

Permalink
Injectable Relations (#166)
Browse files Browse the repository at this point in the history
* add model extensions

* fix method-call-style relations

* clean up code

* add fascade

* add fascade

* remove facade, add to model

* remove unused imports

* add documentation

* remove imports

* add tests

* styleci

* fix injected relations

* add more tests

* add testbench dependency

* test eager loading
  • Loading branch information
recursivetree authored Nov 9, 2023
1 parent c367d47 commit 673c6cf
Show file tree
Hide file tree
Showing 17 changed files with 629 additions and 17 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"psr/log": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
"phpunit/phpunit": "^10.0",
"orchestra/testbench": "^8.0"
},
"extra": {
"laravel": {
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<testsuite name="Report Parser Test Suite">
<directory>./tests/ReportParser</directory>
</testsuite>
<testsuite name="Injected Relations Test Suite">
<directory>./tests/InjectedRelations</directory>
</testsuite>
<testsuite name="CCP HTML CLeaning Test Suite">
<directory>./tests/CleanCCPHtml</directory>
</testsuite>
Expand Down
30 changes: 30 additions & 0 deletions src/Exceptions/InjectedRelationConflictException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Services\Exceptions;

use Exception;

class InjectedRelationConflictException extends Exception
{

}
127 changes: 127 additions & 0 deletions src/Models/ExtensibleModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Services\Models;

use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use LogicException;
use Seat\Services\Exceptions\InjectedRelationConflictException;
use Seat\Services\Services\InjectedRelationRegistry;

abstract class ExtensibleModel extends Model
{
/**
* Returns an attribute or relation of the model, considering injected relations.
*
* @param string $key
* @return mixed
*
* @throws BindingResolutionException
*/
public function __get($key)
{
// fetch injected relations
$extension_registry = app()->make(InjectedRelationRegistry::class);
$extension_class = $extension_registry->getExtensionClassFor($this::class, $key);

// check if we have an injected relation
if($extension_class){
// we have an injected relation
// since what we are doing is not intended, we have to roughly reimplement laravel's code from here on

//check if relation data is cached
// the relation cache continues to work even for 'fake' relations, but we have to manually call it
if($this->relationLoaded($key)){
return $this->getRelationValue($key);
}

// it is NOT cached, we have to load it and put it into the cache
// get relation from extension
$extension_class_instance = new $extension_class;
$relation = $extension_class_instance->$key($this);

// the following code is taken from laravel's \Illuminate\Database\Eloquent\Concerns\HasAttributes::getRelationshipFromMethod
// check if we actually got a relation returned
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $key
));
}

throw new LogicException(sprintf(
'%s::%s must return a relationship instance.', static::class, $key
));
}

return tap($relation->getResults(), function ($results) use ($key) {
$this->setRelation($key, $results);
});
}

// use the default behaviour if no relation is injected
return parent::__get($key);
}

/**
* Redirects calls to injected relations or behaves like a normal class.
*
* @param string $method
* @param $parameters
* @return mixed
*
* @throws BindingResolutionException
*/
public function __call($method, $parameters)
{
// fetch injected relations
$extension_registry = app()->make(InjectedRelationRegistry::class);
$extension_class = $extension_registry->getExtensionClassFor($this::class, $method);

// check if we have an injected relation
if($extension_class) {
// return the injected relation
$extension_class_instance = new $extension_class;

return $extension_class_instance->$method($this);
}

// use the default behaviour if no relation is injected
return parent::__call($method, $parameters);
}

/**
* Injects relations into this model.
*
* @param string $extension_class the class that provides the injected relations
* @return void
*
* @throws BindingResolutionException
* @throws InjectedRelationConflictException A conflict arises when trying to inject two relations with the same name into a target.
*/
public static function injectRelationsFrom(string $extension_class): void {
$registry = app()->make(InjectedRelationRegistry::class);
$registry->injectRelations(static::class, $extension_class);
}
}
6 changes: 2 additions & 4 deletions src/Models/GlobalSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/*
* This file is part of SeAT
*
* Copyright (C) 2015 to 2022 Leon Jacobs
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -22,14 +22,12 @@

namespace Seat\Services\Models;

use Illuminate\Database\Eloquent\Model;

/**
* Class GlobalSetting.
*
* @package Seat\Services\Models
*/
class GlobalSetting extends Model
class GlobalSetting extends ExtensibleModel
{
/**
* @var array
Expand Down
5 changes: 2 additions & 3 deletions src/Models/Note.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/*
* This file is part of SeAT
*
* Copyright (C) 2015 to 2022 Leon Jacobs
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -23,14 +23,13 @@
namespace Seat\Services\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

/**
* Class Note.
*
* @package Seat\Services\Models
*/
class Note extends Model
class Note extends ExtensibleModel
{
/**
* @var array
Expand Down
6 changes: 2 additions & 4 deletions src/Models/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/*
* This file is part of SeAT
*
* Copyright (C) 2015 to 2022 Leon Jacobs
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -22,14 +22,12 @@

namespace Seat\Services\Models;

use Illuminate\Database\Eloquent\Model;

/**
* Class Schedule.
*
* @package Seat\Services\Models
*/
class Schedule extends Model
class Schedule extends ExtensibleModel
{
/**
* @var array
Expand Down
6 changes: 2 additions & 4 deletions src/Models/UserSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/*
* This file is part of SeAT
*
* Copyright (C) 2015 to 2022 Leon Jacobs
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -22,14 +22,12 @@

namespace Seat\Services\Models;

use Illuminate\Database\Eloquent\Model;

/**
* Class UserSetting.
*
* @package Seat\Services\Models
*/
class UserSetting extends Model
class UserSetting extends ExtensibleModel
{
/**
* @var array
Expand Down
100 changes: 100 additions & 0 deletions src/Services/InjectedRelationRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Services\Services;

use Seat\Services\Exceptions\InjectedRelationConflictException;

class InjectedRelationRegistry
{
/**
* Lookup table to search for injected relations.
* Maps the result from getInjectionTargetKey to a class.
*
* @var array<string, string>
*/
private array $relations = [];

/**
* Injects all relations from a class into a model.
*
* @param string $target_model the model to inject relations into
* @param string $extension_class the class to take the relations from
* @return void
*
* @throws InjectedRelationConflictException A conflict arises when trying to inject two relations with the same name into a target.
*/
public function injectRelations(string $target_model, string $extension_class): void
{
$methods = get_class_methods($extension_class);

foreach ($methods as $relation_name){
$this->injectSingleRelation($target_model, $extension_class, $relation_name);
}
}

/**
* Injects a single relation into a model.
*
* @param string $model the model to inject the relation into
* @param string $extension_class the class holding the relation function
* @param string $relation the name of the relation to be injected. The method providing the relation in $extension_class must have the same name, and the relation will be accessible under this name.
* @return void
*
* @throws InjectedRelationConflictException A conflict arises when trying to inject two relations with the same name into a target.
*/
public function injectSingleRelation(string $model, string $extension_class, string $relation): void
{
$key = $this->getInjectionTargetKey($model, $relation);

// check for conflicts, as there can't be two relations with the same name
if(array_key_exists($key, $this->relations)) {
$conflict = $this->relations[$key];
throw new InjectedRelationConflictException(sprintf('Relation \'%s\' from \'%s\' is name-conflicting with \'%s\'', $relation, $model, $conflict));
}

$this->relations[$key] = $extension_class;
}

/**
* Searches for injected relations for a model.
*
* @param string $model the model to search for
* @param string $relation the relation name to search for
* @return string|null the class providing the injected relation, or null if there is no injected relation
*/
public function getExtensionClassFor(string $model, string $relation): ?string
{
return $this->relations[$this->getInjectionTargetKey($model, $relation)] ?? null;
}

/**
* Generates a key for $this->$relations.
*
* @param string $model the injection target class
* @param string $relation_name the relation name
* @return string a key to use with $this->$relations
*/
private function getInjectionTargetKey(string $model, string $relation_name): string {
return sprintf('%s.%s', $model, $relation_name);
}
}
Loading

0 comments on commit 673c6cf

Please sign in to comment.