mirror of
https://github.com/flarum/framework.git
synced 2025-02-18 20:42:44 +08:00
Add model extender (#2100)
This covers default attribute values, date attributes and custom relationships.
This commit is contained in:
parent
173a698fb4
commit
ef44ff5603
|
@ -14,6 +14,7 @@ use Flarum\Event\ConfigureModelDefaultAttributes;
|
|||
use Flarum\Event\GetModelRelationship;
|
||||
use Illuminate\Database\Eloquent\Model as Eloquent;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Arr;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
|
@ -46,6 +47,12 @@ abstract class AbstractModel extends Eloquent
|
|||
*/
|
||||
protected $afterDeleteCallbacks = [];
|
||||
|
||||
public static $customRelations = [];
|
||||
|
||||
public static $dateAttributes = [];
|
||||
|
||||
public static $defaults = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -71,13 +78,16 @@ abstract class AbstractModel extends Eloquent
|
|||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$defaults = [];
|
||||
$this->attributes = Arr::get(static::$defaults, static::class, []);
|
||||
|
||||
// Deprecated in beta 13, remove in beta 14.
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureModelDefaultAttributes($this, $defaults)
|
||||
new ConfigureModelDefaultAttributes($this, $this->attributes)
|
||||
);
|
||||
|
||||
$this->attributes = $defaults;
|
||||
$this->attributes = array_map(function ($item) {
|
||||
return is_callable($item) ? $item() : $item;
|
||||
}, $this->attributes);
|
||||
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
@ -89,19 +99,11 @@ abstract class AbstractModel extends Eloquent
|
|||
*/
|
||||
public function getDates()
|
||||
{
|
||||
static $dates = [];
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureModelDates($this, $this->dates)
|
||||
);
|
||||
|
||||
$class = get_class($this);
|
||||
|
||||
if (! isset($dates[$class])) {
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureModelDates($this, $this->dates)
|
||||
);
|
||||
|
||||
$dates[$class] = $this->dates;
|
||||
}
|
||||
|
||||
return $dates[$class];
|
||||
return array_merge($this->dates, Arr::get(static::$dateAttributes, static::class, []));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,6 +141,13 @@ abstract class AbstractModel extends Eloquent
|
|||
*/
|
||||
protected function getCustomRelation($name)
|
||||
{
|
||||
$relation = Arr::get(static::$customRelations, static::class.".$name", null);
|
||||
|
||||
if (! is_null($relation)) {
|
||||
return $relation($this);
|
||||
}
|
||||
|
||||
// Deprecated, remove in beta 14
|
||||
return static::$dispatcher->until(
|
||||
new GetModelRelationship($this, $name)
|
||||
);
|
||||
|
|
|
@ -12,6 +12,8 @@ namespace Flarum\Event;
|
|||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 13, removed in beta 14
|
||||
*
|
||||
* The `ConfigureModelDates` event is called to retrieve a list of fields for a model
|
||||
* that should be converted into date objects.
|
||||
*/
|
||||
|
|
|
@ -11,6 +11,9 @@ namespace Flarum\Event;
|
|||
|
||||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 13, removed in beta 14
|
||||
*/
|
||||
class ConfigureModelDefaultAttributes
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,8 @@ namespace Flarum\Event;
|
|||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated beta 13, use the Model extender instead.
|
||||
*
|
||||
* The `GetModelRelationship` event is called to retrieve Relation object for a
|
||||
* model. Listeners should return an Eloquent Relation object.
|
||||
*/
|
||||
|
|
171
framework/core/src/Extend/Model.php
Normal file
171
framework/core/src/Extend/Model.php
Normal file
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Extend;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Model implements ExtenderInterface
|
||||
{
|
||||
private $modelClass;
|
||||
private $dateAttributes = [];
|
||||
private $defaults = [];
|
||||
private $relationships = [];
|
||||
|
||||
/**
|
||||
* @param string $modelClass The ::class attribute of the model you are modifying.
|
||||
* This model should extend from \Flarum\Database\AbstractModel.
|
||||
*/
|
||||
public function __construct(string $modelClass)
|
||||
{
|
||||
$this->modelClass = $modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an attribute to be treated as a date.
|
||||
*
|
||||
* @param string $attribute
|
||||
*/
|
||||
public function dateAttribute(string $attribute)
|
||||
{
|
||||
Arr::set(AbstractModel::$dateAttributes, $this->modelClass, array_merge(Arr::get(AbstractModel::$dateAttributes, $this->modelClass, []), [$attribute]));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default value for a given attribute, which can be an explicit value, or a closure.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function default(string $attribute, $value)
|
||||
{
|
||||
Arr::set(AbstractModel::$defaults, "$this->modelClass.$attribute", $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship from this model to another model.
|
||||
*
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param callable $callable
|
||||
*
|
||||
* The callable can be a closure or invokable class, and should accept:
|
||||
* - $instance: An instance of this model.
|
||||
*
|
||||
* The callable should return:
|
||||
* - $relationship: A Laravel Relationship object. See relevant methods of models
|
||||
* like \Flarum\User\User for examples of how relationships should be returned.
|
||||
*/
|
||||
public function relationship(string $name, callable $callable)
|
||||
{
|
||||
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple belongsTo relationship from this model to another model.
|
||||
* This represents an inverse one-to-one or inverse one-to-many relationship.
|
||||
* For more complex relationships, use the ->relationship method.
|
||||
*
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
|
||||
* @param string $foreignKey: The foreign key attribute of the parent model.
|
||||
* @param string $ownerKey: The primary key attribute of the parent model.
|
||||
*/
|
||||
public function belongsTo(string $name, $related, $foreignKey = null, $ownerKey = null)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $ownerKey, $name) {
|
||||
return $model->belongsTo($related, $foreignKey, $ownerKey, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple belongsToMany relationship from this model to another model.
|
||||
* This represents a many-to-many relationship.
|
||||
* For more complex relationships, use the ->relationship method.
|
||||
*
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
|
||||
* @param string $table: The intermediate table for this relation
|
||||
* @param string $foreignPivotKey: The foreign key attribute of the parent model.
|
||||
* @param string $relatedPivotKey: The associated key attribute of the relation.
|
||||
* @param string $parentKey: The key name of the parent model.
|
||||
* @param string $relatedKey: The key name of the related model.
|
||||
*/
|
||||
public function belongsToMany(
|
||||
string $name,
|
||||
$related,
|
||||
$table = null,
|
||||
$foreignPivotKey = null,
|
||||
$relatedPivotKey = null,
|
||||
$parentKey = null,
|
||||
$relatedKey = null
|
||||
)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractModel $model) use ($related, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $name) {
|
||||
return $model->belongsToMany($related, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasOne relationship from this model to another model.
|
||||
* This represents a one-to-one relationship.
|
||||
* For more complex relationships, use the ->relationship method.
|
||||
*
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
|
||||
* @param string $foreignKey: The foreign key attribute of the parent model.
|
||||
* @param string $localKey: The primary key attribute of the parent model.
|
||||
*/
|
||||
public function hasOne(string $name, $related, $foreignKey = null, $localKey = null)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $localKey) {
|
||||
return $model->hasOne($related, $foreignKey, $localKey);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a simple hasMany relationship from this model to another model.
|
||||
* This represents a one-to-many relationship.
|
||||
* For more complex relationships, use the ->relationship method.
|
||||
*
|
||||
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
|
||||
* but has to be unique from other relation names for this model, and should
|
||||
* work as the name of a method.
|
||||
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
|
||||
* @param string $foreignKey: The foreign key attribute of the parent model.
|
||||
* @param string $localKey: The primary key attribute of the parent model.
|
||||
*/
|
||||
public function hasMany(string $name, $related, $foreignKey = null, $localKey = null)
|
||||
{
|
||||
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $localKey) {
|
||||
return $model->hasMany($related, $foreignKey, $localKey);
|
||||
});
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
// Nothing needed here.
|
||||
}
|
||||
}
|
250
framework/core/tests/integration/extenders/ModelTest.php
Normal file
250
framework/core/tests/integration/extenders/ModelTest.php
Normal file
|
@ -0,0 +1,250 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Tests\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class ModelTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function prepDb()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->adminUser(),
|
||||
$this->normalUser(),
|
||||
],
|
||||
'discussions' => []
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_does_not_exist_by_default()
|
||||
{
|
||||
$this->prepDB();
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$user->customRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->relationship('customRelation', function (User $user) {
|
||||
return $user->hasMany(Discussion::class, 'user_id');
|
||||
}));
|
||||
|
||||
$this->prepDB();
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->assertEquals([], $user->customRelation()->get()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_exists_and_can_return_instances_if_added()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->relationship('customRelation', function (User $user) {
|
||||
return $user->hasMany(Discussion::class, 'user_id');
|
||||
}));
|
||||
|
||||
$this->prepDB();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1]
|
||||
]
|
||||
]);
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->assertNotEquals([], $user->customRelation()->get()->toArray());
|
||||
$this->assertContains(json_encode(__CLASS__), json_encode($user->customRelation()->get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_relationship_does_not_exist_if_added_to_unrelated_model()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->relationship('customRelation', function (User $user) {
|
||||
return $user->hasMany(Discussion::class, 'user_id');
|
||||
}));
|
||||
|
||||
$this->prepDB();
|
||||
$this->prepareDatabase([
|
||||
'groups' => [
|
||||
$this->adminGroup()
|
||||
]
|
||||
]);
|
||||
|
||||
$group = Group::find(1);
|
||||
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$group->customRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_hasOne_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->hasMany('customRelation', Discussion::class, 'user_id'));
|
||||
|
||||
$this->prepDB();
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->assertEquals([], $user->customRelation()->get()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_hasMany_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->hasMany('customRelation', Discussion::class, 'user_id'));
|
||||
|
||||
$this->prepDB();
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->assertEquals([], $user->customRelation()->get()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_belongsTo_relationship_exists_if_added()
|
||||
{
|
||||
$this->extend((new Extend\Model(User::class))->belongsTo('customRelation', Discussion::class, 'user_id'));
|
||||
|
||||
$this->prepDB();
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$this->assertEquals([], $user->customRelation()->get()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_default_attribute_doesnt_exist_if_not_set()
|
||||
{
|
||||
$group = new Group;
|
||||
|
||||
$this->app();
|
||||
|
||||
$this->assertNotEquals('Custom Default', $group->name_singular);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_default_attribute_works_if_set()
|
||||
{
|
||||
$this->extend((new Extend\Model(Group::class))->default('name_singular', 'Custom Default'));
|
||||
|
||||
$this->app();
|
||||
|
||||
$group = new Group;
|
||||
|
||||
$this->assertEquals('Custom Default', $group->name_singular);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_default_attribute_evaluated_at_runtime_if_callable()
|
||||
{
|
||||
$time = Carbon::now();
|
||||
$this->extend((new Extend\Model(Group::class))->default('name_singular', function () {
|
||||
return Carbon::now();
|
||||
}));
|
||||
|
||||
$this->app();
|
||||
|
||||
sleep(2);
|
||||
|
||||
$group = new Group;
|
||||
|
||||
$this->assertGreaterThanOrEqual($time->diffInSeconds($group->name_singular), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_default_attribute_doesnt_work_if_set_on_unrelated_model()
|
||||
{
|
||||
$this->extend((new Extend\Model(Group::class))->default('name_singular', 'Custom Default'));
|
||||
|
||||
$this->app();
|
||||
|
||||
$user = new User;
|
||||
|
||||
$this->assertNotEquals('Custom Default', $user->name_singular);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_date_attribute_doesnt_exist_by_default()
|
||||
{
|
||||
$post = new Post;
|
||||
|
||||
$this->app();
|
||||
|
||||
$this->assertNotContains('custom', $post->getDates());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_date_attribute_can_be_set()
|
||||
{
|
||||
$this->extend((new Extend\Model(Post::class))->dateAttribute('custom'));
|
||||
|
||||
$this->app();
|
||||
|
||||
$post = new Post;
|
||||
|
||||
$this->assertContains('custom', $post->getDates());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function custom_date_attribute_doesnt_work_if_set_on_unrelated_model()
|
||||
{
|
||||
$this->extend((new Extend\Model(Post::class))->dateAttribute('custom'));
|
||||
|
||||
$this->app();
|
||||
|
||||
$discussion = new Discussion;
|
||||
|
||||
$this->assertNotContains('custom', $discussion->getDates());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user