lundi 27 mars 2017

Does this concept to add methods to an existing PHP interface scale?

I am using Nicolas Widart's Laravel Modules package to help manage a large app, and keep everything separated into logical modules. I would like to be able to drop in different modules and have them play nicely without any extra configuration.

All of my modules will define interfaces and default implementations that allow the application (the system controlling which modules are loaded) to specify that it wants to use a specific implementation instead, through dependency injection.

I am able to make some assumptions by having some modules require others, for example a payment processing module (Module PP) can assume that a payment is tied to a user (with which the interface for a user is defined in another module, Module U).

My ideal scenario is that I could add to an existing PHP interface that is defined in another required module. For example, being able to retrieve a user from a repository defined in Module U and call a method on it that was defined in Module PP.

Once Module PP resolves the interface (again, through dependency injection) from Module U to a class, I want my method from Module PP to be callable on that class.

I have been able to achieve this using the __call magic method as below.


Extensions Module

This module defines the core operations to add to an existing interface.

IsExtendable Interface

<?php

namespace Modules\Extensions\Contracts;

interface IsExtendable
{
    /**
     * Get the list of extensions for this entity.
     *
     * @return array
     */
    public static function getExtensions();

    /**
     * Adds an extension to this entity.
     *
     * @param string $name
     * @param mixed  $function
     */
    public static function addExtension($name, $function);

    /**
     * Checks whether the entity has the given extension.
     *
     * @param string $name
     *
     * @return bool
     */
    public static function hasExtension($name);

    /**
     * Call the extension if it exists, or pass it further up the chain.
     *
     * @param string $name
     * @param mixed $arguments
     *
     * @return mixed
     */
    public function __call($name, $arguments);
}

IsExtendable Trait

<?php

namespace Modules\Extensions;

trait IsExtendable
{
    /** @var $extensions */
    private static $extensions = [];

    /**
     * Get the list of extensions for this entity.
     *
     * @return array
     */
    public static function getExtensions()
    {
        return self::$extensions;
    }

    /**
     * Adds an extension to this entity.
     *
     * @param string $name
     * @param mixed  $function
     */
    public static function addExtension($name, $function)
    {
        if(is_callable($function) == FALSE)
        {
            throw new \InvalidArgumentException('Function must be callable.');
        }

        self::$extensions[$name] = $function;
    }

    /**
     * Checks whether the entity has the given extension.
     *
     * @param string $name
     *
     * @return bool
     */
    public static function hasExtension($name)
    {
        return array_key_exists($name, self::getExtensions()) == TRUE;
    }

    /**
     * Calls the extension if it exists, or passes it further up the chain.
     *
     * @param string $name
     * @param mixed  $arguments
     *
     * @return mixed
     */
    public function __call($name, $arguments)
    {
        if(self::hasExtension($name) == TRUE)
        {
            $callable = self::getExtensions()[$name];

            return call_user_func_array($callable, array_merge(array($this), $arguments));
        }

        else
        {
            return parent::__call($name, $arguments);
        }
    }
}

Service Provider

<?php

namespace Modules\Extensions\Providers;

use Illuminate\Support\ServiceProvider;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;

class ExtensionServiceProvider extends ServiceProvider
{
    /**
     * @param string $implementation
     * @param string $functionName
     *
     * @return callable
     */
    public function prepareExtension($implementation, $functionName)
    {
        return $implementation . '::' . $functionName;
    }

    /**
     * @param string $contract
     * @param string $implementation
     *
     * @return void
     */
    public function extractExtensions($contract, $implementation)
    {
        $reflection = new \ReflectionClass($implementation);

        $methods = [];

        foreach($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method)
        {
            // TODO: May be able to use $method->getClosure() here
            // http://ift.tt/2nEGWLp
            $methods[] = $method->getName();
        }

        $this->registerExtensions($contract, $methods, $implementation);
    }

    /**
     * @param string $contract
     * @param string $name
     * @param string $function
     *
     * @return void
     */
    public function registerExtension($contract, $name, $function)
    {
        // Resolve the contract to an implementation
        $base = app($contract);

        // Check that it is suitable for extension
        if($base instanceof IsExtendableContract)
        {
            $base::addExtension($name, $function);
        }
    }

    /**
     * @param string      $contract
     * @param array       $extensions
     * @param string|null $implementation
     *
     * @return void
     */
    public function registerExtensions($contract, array $extensions = [], $implementation = NULL)
    {
        // Resolve the contract to an implementation
        $base = app($contract);

        // Check that it is suitable for extension
        if($base instanceof IsExtendableContract)
        {
            foreach($extensions as $name => $function)
            {
                if(is_int($name) == TRUE)
                {
                    if(is_string($function) == TRUE)
                    {
                        $name = $function;
                    }

                    else
                    {
                        throw new \InvalidArgumentException('All extensions must have a valid name.');
                    }
                }

                if(is_string($function) == TRUE)
                {
                    if(strpos($function, '::') === FALSE && $implementation != NULL)
                    {
                        $function = $this->prepareExtension($implementation, $function);
                    }
                }

                $base::addExtension($name, $function);
            }
        }
    }
}


Module U

User Interface

<?php

namespace Modules\Auth\Contracts\Entities;

interface User
{
    /**
     * @return int
     */
    public function getId();

    /**
     * @return string
     */
    public function getName();

    /**
     * @return string
     */
    public function getEmail();

    /**
     * @return \DateTime
     */
    public function getCreatedAt();

    /**
     * @return \DateTime
     */
    public function getUpdatedAt();
}

User Implementation

<?php

namespace Modules\Auth\Entities;

use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Extensions\IsExtendable;

class User implements
    IsExtendableContract,
    UserContract
{
    use IsExtendable;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->created_at;
    }

    /**
     * @return \DateTime
     */
    public function getUpdatedAt()
    {
        return $this->updated_at;
    }
}


Module PP

User Extension

<?php

namespace Modules\Test\Entities\Extensions;

use Modules\Auth\Contracts\Entities\User;

class UserExtension
{
    /**
     * @param User $context
     */
    public static function getCardLastFour($context)
    {
        return $context->card_last_four;
    }

    /**
     * @param User $context
     */
    public static function getCardBrand($context)
    {
        return $context->card_brand;
    }

    /**
     * @param User $context
     */
    public static function getStripeId($context)
    {
        return $context->stripe_id;
    }
}

Service Provider

<?php

namespace Modules\Test\Providers\Extensions;

use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Test\Entities\Extensions\UserExtension;

use Modules\Extensions\Providers\ExtensionServiceProvider;

class StripeExtensionProvider extends ExtensionServiceProvider
{
    public function boot()
    {
        // TODO: Set the contract as a static field on the extension to then automatically extract from all extension files in a folder
        $this->extractExtensions(UserContract::class, UserExtension::class);
    }
}


My question is, is this method scalable (across maybe 10 modules), and can you foresee any issues with it? Or is there a better/more popular (and supported) way to do this? I don't want to get 2 years into a project and discover that I really hate the way I've implemented this.

I know that this concept won't support IDE autocompletion out of the box but I could build in a way to generate the PHPDocs similar to this package.

I have researched the Decorator pattern but this feels clunky in that I would always need to rely on a new implementation within each module, instead of just adding to the existing one.

I realise this is a big question so my sincere thanks to anyone willing to have a look at it!



from Newest questions tagged laravel-5 - Stack Overflow http://ift.tt/2o9kF5U
via IFTTT

Aucun commentaire:

Enregistrer un commentaire