Fix Callables for Extenders (#2423)

- Standardize signatures and variable names for extenders that take callbacks
- Adjust model extender docblock to clarify that default calue can't be an invokable class.
- Make invokable classes provided to Model->relationship
- Add integration tests to ensure Model->relationship and User->groupProcessor extenders accept callbacks
- Extract code for wrapping callbacks into central util
This commit is contained in:
Alexander Skvortsov 2020-11-08 21:36:38 -05:00 committed by GitHub
parent de5d9644cf
commit 74946a3d04
10 changed files with 108 additions and 39 deletions

View File

@ -25,7 +25,7 @@ class Event implements ExtenderInterface
* - the class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter * - the class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter
* *
* @param string $event * @param string $event
* @param callable $listener * @param callable|string $listener
*/ */
public function listen(string $event, $listener) public function listen(string $event, $listener)
{ {

View File

@ -11,6 +11,7 @@ namespace Flarum\Extend;
use Flarum\Extension\Extension; use Flarum\Extension\Extension;
use Flarum\Formatter\Formatter as ActualFormatter; use Flarum\Formatter\Formatter as ActualFormatter;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
class Formatter implements ExtenderInterface, LifecycleInterface class Formatter implements ExtenderInterface, LifecycleInterface
@ -83,27 +84,15 @@ class Formatter implements ExtenderInterface, LifecycleInterface
{ {
$container->extend('flarum.formatter', function ($formatter, $container) { $container->extend('flarum.formatter', function ($formatter, $container) {
foreach ($this->configurationCallbacks as $callback) { foreach ($this->configurationCallbacks as $callback) {
if (is_string($callback)) { $formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
$callback = $container->make($callback);
}
$formatter->addConfigurationCallback($callback);
} }
foreach ($this->parsingCallbacks as $callback) { foreach ($this->parsingCallbacks as $callback) {
if (is_string($callback)) { $formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
$callback = $container->make($callback);
}
$formatter->addParsingCallback($callback);
} }
foreach ($this->renderingCallbacks as $callback) { foreach ($this->renderingCallbacks as $callback) {
if (is_string($callback)) { $formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
$callback = $container->make($callback);
}
$formatter->addRenderingCallback($callback);
} }
return $formatter; return $formatter;

View File

@ -171,11 +171,7 @@ class Frontend implements ExtenderInterface
"flarum.frontend.$this->frontend", "flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) { function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) { foreach ($this->content as $content) {
if (is_string($content)) { $frontend->content(ContainerUtil::wrapCallback($content, $container));
$content = $container->make($content);
}
$frontend->content($content);
} }
} }
); );

View File

@ -11,12 +11,14 @@ namespace Flarum\Extend;
use Flarum\Database\AbstractModel; use Flarum\Database\AbstractModel;
use Flarum\Extension\Extension; use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
class Model implements ExtenderInterface class Model implements ExtenderInterface
{ {
private $modelClass; private $modelClass;
private $customRelations = [];
/** /**
* @param string $modelClass The ::class attribute of the model you are modifying. * @param string $modelClass The ::class attribute of the model you are modifying.
@ -48,7 +50,9 @@ class Model implements ExtenderInterface
} }
/** /**
* Add a default value for a given attribute, which can be an explicit value, or a closure. * Add a default value for a given attribute, which can be an explicit value, a closure,
* or an instance of an invokable class. Unlike with some other extenders,
* it CANNOT be the `::class` attribute of an invokable class.
* *
* @param string $attribute * @param string $attribute
* @param mixed $value * @param mixed $value
@ -157,7 +161,7 @@ class Model implements ExtenderInterface
* @param string $name: The name of the relation. This doesn't have to be anything in particular, * @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 * but has to be unique from other relation names for this model, and should
* work as the name of a method. * work as the name of a method.
* @param callable $callable * @param callable|string $callback
* *
* The callable can be a closure or invokable class, and should accept: * The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model. * - $instance: An instance of this model.
@ -168,15 +172,17 @@ class Model implements ExtenderInterface
* *
* @return self * @return self
*/ */
public function relationship(string $name, callable $callable) public function relationship(string $name, $callback)
{ {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable); $this->customRelations[$name] = $callback;
return $this; return $this;
} }
public function extend(Container $container, Extension $extension = null) public function extend(Container $container, Extension $extension = null)
{ {
// Nothing needed here. foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
}
} }
} }

View File

@ -35,7 +35,7 @@ class User implements ExtenderInterface
* This can be used to give a user permissions for groups they aren't actually in, based on context. * This can be used to give a user permissions for groups they aren't actually in, based on context.
* It will not change the group badges displayed for the user. * It will not change the group badges displayed for the user.
* *
* @param callable $callable * @param callable|string $callback
* *
* The callable can be a closure or invokable class, and should accept: * The callable can be a closure or invokable class, and should accept:
* - \Flarum\User\User $user: the user in question. * - \Flarum\User\User $user: the user in question.
@ -44,9 +44,9 @@ class User implements ExtenderInterface
* The callable should return: * The callable should return:
* - array $groupIds: an array of ids for the groups the user belongs to. * - array $groupIds: an array of ids for the groups the user belongs to.
*/ */
public function permissionGroups(callable $callable) public function permissionGroups($callback)
{ {
$this->groupProcessors[] = $callable; $this->groupProcessors[] = $callback;
return $this; return $this;
} }

View File

@ -10,6 +10,7 @@
namespace Flarum\Extend; namespace Flarum\Extend;
use Flarum\Extension\Extension; use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
class Validator implements ExtenderInterface class Validator implements ExtenderInterface
@ -47,11 +48,7 @@ class Validator implements ExtenderInterface
{ {
$container->resolving($this->validator, function ($validator, $container) { $container->resolving($this->validator, function ($validator, $container) {
foreach ($this->configurationCallbacks as $callback) { foreach ($this->configurationCallbacks as $callback) {
if (is_string($callback)) { $validator->addConfiguration(ContainerUtil::wrapCallback($callback, $container));
$callback = $container->make($callback);
}
$validator->addConfiguration($callback);
} }
}); });
} }

View File

@ -0,0 +1,36 @@
<?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\Foundation;
use Illuminate\Contracts\Container\Container;
class ContainerUtil
{
/**
* Wraps a callback so that string-based invokable classes get resolved only when actually used.
*
* @internal Backwards compatability not guaranteed.
*
* @param callable|string $callback: A callable, or a ::class attribute of an invokable class
* @param Container $container
*/
public static function wrapCallback($callback, Container $container)
{
if (is_string($callback)) {
$callback = function () use ($container, $callback) {
$callback = $container->make($callback);
return call_user_func_array($callback, func_get_args());
};
}
return $callback;
}
}

View File

@ -11,6 +11,7 @@ namespace Flarum\User;
use Flarum\Event\ConfigureUserPreferences; use Flarum\Event\ConfigureUserPreferences;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ContainerUtil;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\DisplayName\DriverInterface; use Flarum\User\DisplayName\DriverInterface;
use Flarum\User\DisplayName\UsernameDriver; use Flarum\User\DisplayName\UsernameDriver;
@ -77,11 +78,7 @@ class UserServiceProvider extends AbstractServiceProvider
public function boot() public function boot()
{ {
foreach ($this->app->make('flarum.user.group_processors') as $callback) { foreach ($this->app->make('flarum.user.group_processors') as $callback) {
if (is_string($callback)) { User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->app));
$callback = $this->app->make($callback);
}
User::addGroupProcessor($callback);
} }
User::setHasher($this->app->make('hash')); User::setHasher($this->app->make('hash'));

View File

@ -134,6 +134,23 @@ class ModelTest extends TestCase
$this->assertEquals([], $user->customRelation()->get()->toArray()); $this->assertEquals([], $user->customRelation()->get()->toArray());
} }
/**
* @test
*/
public function custom_relationship_can_be_invokable_class()
{
$this->extend(
(new Extend\Model(User::class))
->relationship('customRelation', CustomRelationClass::class)
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/** /**
* @test * @test
*/ */
@ -423,3 +440,11 @@ class ModelTestCustomPost extends AbstractEventPost
*/ */
public static $type = 'customPost'; public static $type = 'customPost';
} }
class CustomRelationClass
{
public function __invoke(User $user)
{
return $user->hasMany(Discussion::class, 'user_id');
}
}

View File

@ -88,6 +88,19 @@ class UserTest extends TestCase
$this->assertNotContains('viewUserList', $user->getPermissions()); $this->assertNotContains('viewUserList', $user->getPermissions());
} }
/**
* @test
*/
public function processor_can_be_invokable_class()
{
$this->extend((new Extend\User)->permissionGroups(CustomGroupProcessorClass::class));
$this->prepDb();
$user = User::find(2);
$this->assertNotContains('viewUserList', $user->getPermissions());
}
} }
class CustomDisplayNameDriver implements DriverInterface class CustomDisplayNameDriver implements DriverInterface
@ -97,3 +110,13 @@ class CustomDisplayNameDriver implements DriverInterface
return $user->email.'$$$suffix'; return $user->email.'$$$suffix';
} }
} }
class CustomGroupProcessorClass
{
public function __invoke(User $user, array $groupIds)
{
return array_filter($groupIds, function ($id) {
return $id != 3;
});
}
}