Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue with getting rules #110

Closed
wants to merge 1 commit into from
Closed

Conversation

solleer
Copy link
Contributor

@solleer solleer commented Jun 10, 2016

If the default rule was set or the class was set and you used a named instance, the defaults from the class and default would not be carried down.

If the default rule was set or the class was set and you used a named instance, the defaults from the class and default would not be carried down.
@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Can you post some sample code that demonstrates the problem? I don't think I'm understanding where the issue it

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

Config

{
    "Transphporm\\Builder" : {
        "call" : [
            ["loadModule", [{"instance" : "TransphpormNl2br\\Module"}]],
            ["loadModule", [{"instance" : "TransphpormMessages\\Module"}]]
        ]
    }
}

rules added later

"$view" : {
     "instanceOf" : "Transphporm\\Builder",
      "constructParams" : ["Layouts/layout.xml", "{dir}/view/form/signout.tss"]
}

The modules are not made when $view is created

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

I'm not sure this should be the expected behaviour in this case. If you're using a named instance, it's a specific instance with specific rules. The problem with this approach is because of array_merge_recursive if you wanted a named instance where loadModule was called with different arguments than the default it would call the defaults, then the additional arguments e.g.

{
    "Transphporm\\Builder" : {
        "call" : [
            ["loadModule", [{"instance" : "TransphpormNl2br\\Module"}]],
            ["loadModule", [{"instance" : "TransphpormMessages\\Module"}]]
        ]
    }
}

Bur I want a different one instance where these modules are not loaded but a different one was instead:

"$specialistBuilder" : {
     "instanceOf" : "Transphporm\\Builder",
      "constructParams" : ["Layouts/layout.xml", "{dir}/view/form/signout.tss"],
     "call" : [
            ["loadModule", [{"instance" : "Transphporm\\SomeOtherModule"}]]
        ]
}

This would load all three modules, which wouldn't be what I'd expect here. Isn't a better solution just to add the call property to the named instance?

"$view" : {
     "instanceOf" : "Transphporm\\Builder",
      "constructParams" : ["Layouts/layout.xml", "{dir}/view/form/signout.tss"],
      "call" : [
            ["loadModule", [{"instance" : "TransphpormNl2br\\Module"}]],
            ["loadModule", [{"instance" : "TransphpormMessages\\Module"}]]
        ]
}

This is more flexible, but obviously would require specifying the call option for any named instance of that type.

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

The issue is having to write those same calls for every single instance and I have a lot of them since I am using the json router module

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

Could it be changed to if the inherit property on a named instance is true then it gets the rules from instanceOf and if on the named instance inherit is false then it works as it does now

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Actually you're right. inherit can already be used to prevent the rules being inherited so your original suggestion makes sense.

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

I'm going to tweak this slightly as not returning the rule if there is an exact match and looping over all the rules will be an unnecessary performance hit.

edit: Merge needs to be done in addrule, ideally

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Can you tell me if this works as an alternative?

    public function addRule($name, array $rule) {
        if (isset($rule['instanceOf'])) $rule = array_merge_recursive($this->getRule($rule['instanceOf']), $rule);
        $this->rules[ltrim(strtolower($name), '\\')] = array_merge_recursive($this->getRule($name), $rule);
    }

It should be faster than needing to loop through all the rules every single time that getRule is called.

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

That should work but would not support your case where they don't want them to be inherited

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Yes, you're right. This should do it:

    public function addRule($name, array $rule) {
        if (isset($rule['instanceOf']) && !(isset($rule['inherit']) && $rule['inherit'] == 'false')) $rule = array_merge_recursive($this->getRule($rule['instanceOf']), $rule);
        $this->rules[ltrim(strtolower($name), '\\')] = array_merge_recursive($this->getRule($name), $rule);
    }

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

I get this now though

Warning: Illegal offset type in isset or empty in C:\Users\Solle\OneDrive\Documents\Programing Stuff\Web development\serverRoot\elemukulek\lib\Dice\Dice.php on line 63

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

I'm not seeing that error, but this test now fails:

    public function testMultipleSharedInstancesByNameMixed() {
        $rule = [];
        $rule['shared'] = true;
        $rule['constructParams'][] = 'FirstY';

        $this->dice->addRule('Y', $rule);

        $rule = [];
        $rule['instanceOf'] = 'Y';
        $rule['shared'] = true;
        $rule['constructParams'][] = 'SecondY';

        $this->dice->addRule('[Y2]', $rule);

        $rule = [];
        $rule['constructParams'] = [ ['instance' => 'Y'], ['instance' => '[Y2]']];

        $this->dice->addRule('Z', $rule);

        $z = $this->dice->create('Z');
        $this->assertEquals($z->y1->name, 'FirstY');
        $this->assertEquals($z->y2->name, 'SecondY');       
    }

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

I think that test fails because for [Y2] the constructParams becomes['FirstY', 'SecondY']

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

My error happens when it calls expand

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

The test above fails because of the new merge, constructParams gets merged so the constructor doesn't get overridden.

This could potentially cause some BC issues as it's a behaviour change. It's difficult to know whether this will affect anyone but one option that won't break BC is forcing setting inherit to true to trigger the new behaviour so you'd need:

"$view" : {
     "instanceOf" : "Transphporm\\Builder",
      "constructParams" : ["Layouts/layout.xml", "{dir}/view/form/signout.tss"],
      "inherit": true,
}

But this is now inconsistent with the current behaviour of inherit which defaults to true anyway.

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Which line is line 63?

if (!empty($this->instances[$name])) return $this->instances[$name];

?

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

Call Stack

Time

Memory

Function

Location

1 0.0119 253104 {main}( ) ...\index.php:0
2 0.1375 679208 Level2\Router\Router->find( ) ...\index.php:24
3 0.1412 680008 Config\Router\ModuleJson->find( ) ...\Router.php:12
4 0.1523 710544 Dice\Dice->create( ) ...\ModuleJson.php:38
5 0.1613 772984 Dice\Dice->Dice{closure}( ) ...\Dice.php:69
6 0.1613 773360 Dice\Dice->Dice{closure}( ) ...\Dice.php:97
7 0.1613 773832 Dice\Dice->expand( ) ...\Dice.php:176
8 0.3256 796144 Dice\Dice->create( ) ...\Dice.php:135

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Can you post the full dice.php you're using as it's difficult for me to see which line's are being referred to there.

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

<?php
/* @description Dice - A minimal Dependency Injection Container for PHP *
 * @author Tom Butler [email protected] *
 * @copyright 2012-2015 Tom Butler <[email protected]> | https:// r.je/dice.html *
 * @license http:// www.opensource.org/licenses/bsd-license.php BSD License *
 * @version 2.0 */
namespace Dice;
class Dice {
    /**
     * @var array $rules Rules which have been set using addRule()
     */
    private $rules = [];

    /**
     * @var array $cache A cache of closures based on class name so each class is only reflected once
     */
    private $cache = [];

    /**
     * @var array $instances Stores any instances marked as 'shared' so create() can return the same instance
     */
    private $instances = [];

    /**
     * Add a rule $rule to the class $name
     * @param string $name The name of the class to add the rule for
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     */
    public function addRule($name, array $rule) {
        if (isset($rule['instanceOf']) && !(isset($rule['inherit']) && $rule['inherit'] == 'false')) $rule = array_merge_recursive($this->getRule($rule['instanceOf']), $rule);
        $this->rules[ltrim(strtolower($name), '\\')] = array_merge_recursive($this->getRule($name), $rule); var_dump($this->rules[ltrim(strtolower($name), '\\')]);
    }

    /**
     * Returns the rule that will be applied to the class $name when calling create()
     * @param string name The name of the class to get the rules for
     * @return array The rules for the specified class
     */
    public function getRule($name) {
        $lcName = strtolower(ltrim($name, '\\'));
        if (isset($this->rules[$lcName])) return $this->rules[$lcName];

        foreach ($this->rules as $key => $rule) {                           // Find a rule which matches the class described in $name where:
            if (empty($rule['instanceOf'])                                  // It's not a named instance, the rule is applied to a class name
                && $key !== '*'                                             // It's not the default rule
                && is_subclass_of($name, $key)                              // The rule is applied to a parent class
                && (!array_key_exists('inherit', $rule) || $rule['inherit'] === true )) // And that rule should be inherited to subclasses
            return $rule;
        }
        // No rule has matched, return the default rule if it's set
        return isset($this->rules['*']) ? $this->rules['*'] : [];
    }

    /**
     * Returns a fully constructed object based on $name using $args and $share as constructor arguments if supplied
     * @param string name The name of the class to instantiate
     * @param array $args An array with any additional arguments to be passed into the constructor upon instantiation
     * @param array $share Whether or not this class instance be shared, so that the same instance is passed around each time
     * @return object A fully constructed object based on the specified input arguments
     */
    public function create($name, array $args = [], array $share = []) {
        // Is there a shared instance set? Return it. Better here than a closure for this, calling a closure is slower.
        if (!empty($this->instances[$name])) return $this->instances[$name];

        // Create a closure for creating the object if there isn't one already
        if (empty($this->cache[$name])) $this->cache[$name] = $this->getClosure($name, $this->getRule($name));

        // Call the cached closure which will return a fully constructed object of type $name
        return $this->cache[$name]($args, $share);
    }

    /**
     * Returns a closure for creating object $name based on $rule, caching the reflection object for later use
     * @param string $name the Name of the class to get the closure for
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     * @return callable A closure
     */
    private function getClosure($name, array $rule) {
        // Reflect the class and constructor, this should only ever be done once per class and get cached
        $class = new \ReflectionClass(isset($rule['instanceOf']) ? $rule['instanceOf'] : $name);
        $constructor = $class->getConstructor();

        // Create parameter generating function in order to cache reflection on the parameters. This way $reflect->getParameters() only ever gets called once
        $params = $constructor ? $this->getParams($constructor, $rule) : null;

        // Get a closure based on the type of object being created: Shared, normal or constructorless
        if (!empty($rule['shared'])) $closure = function (array $args, array $share) use ($class, $name, $constructor, $params) {
            // Shared instance: create the class without calling the constructor (and write to \$name and $name, see issue #68)
            $this->instances[$name] = $this->instances[ltrim($name, '\\')] = $class->newInstanceWithoutConstructor();

            // Now call this constructor after constructing all the dependencies. This avoids problems with cyclic references (issue #7)
            if ($constructor) $constructor->invokeArgs($this->instances[$name], $params($args, $share));
            return $this->instances[$name];
        };
        else if ($params) $closure = function (array $args, array $share) use ($class, $params) {
            // This class has depenencies, call the $params closure to generate them based on $args and $share
            return new $class->name(...$params($args, $share));
        };
        else $closure = function () use ($class) {
            // No constructor arguments, just instantiate the class
            return new $class->name;
        };
        // If there are shared instances, create them and merge them with shared instances higher up the object graph
        if (isset($rule['shareInstances'])) $closure = function(array $args, array $share) use ($closure, $rule) {
            return $closure($args, array_merge($args, $share, array_map([$this, 'create'], $rule['shareInstances'])));
        };
        // When $rule['call'] is set, wrap the closure in another closure which will call the required methods after constructing the object
        // By putting this in a closure, the loop is never executed unless call is actually set
        return isset($rule['call']) ? function (array $args, array $share) use ($closure, $class, $rule) {
            // Construct the object using the original closure
            $object = $closure($args, $share);

            foreach ($rule['call'] as $call) {
                // Generate the method arguments using getParams() and call the returned closure (in php7 will be ()() rather than __invoke)
                $params = $this->getParams($class->getMethod($call[0]), ['shareInstances' => isset($rule['shareInstances']) ? $rule['shareInstances'] : [] ])->__invoke($this->expand(isset($call[1]) ? $call[1] : []));
                $object->{$call[0]}(...$params);
            }
            return $object;
        } : $closure;
    }

    /**
     * Looks for 'instance' array keys in $param and when found returns an object based on the value see {@link https:// r.je/dice.html#example3-1}
     * @param mixed $param Either a string or an array,
     * @param array $share Whether or not this class instance be shared, so that the same instance is passed around each time
     * @param bool $createFromString
     * @return mixed
     */
    private function expand($param, array $share = [], $createFromString = false) {
        if (is_array($param) && isset($param['instance'])) {
            // Call or return the value sored under the key 'instance'
            // For ['instance' => ['className', 'methodName'] construct the instance before calling it
            if (is_array($param['instance'])) $param['instance'][0] = $this->expand($param['instance'][0], $share, true);
            if (is_callable($param['instance'])) return call_user_func($param['instance'], ...(isset($param['params']) ? $this->expand($param['params']) : []));
            else return $this->create($param['instance'], $share);
        }
        // Recursively search for 'instance' keys in $param
        else if (is_array($param)) foreach ($param as $name => $value) $param[$name] = $this->expand($value, $share);
        // 'instance' wasn't found, return the value unchanged
        return is_string($param) && $createFromString ? $this->create($param) : $param;
    }

    /**
     * Returns a closure that generates arguments for $method based on $rule and any $args passed into the closure
     * @param object $method An instance of ReflectionMethod (see: {@link http:// php.net/manual/en/class.reflectionmethod.php})
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     * @return callable A closure that uses the cached information to generate the arguments for the method
     */
    private function getParams(\ReflectionMethod $method, array $rule) {
        // Cache some information about the parameter in $paramInfo so (slow) reflection isn't needed every time
        $paramInfo = [];
        foreach ($method->getParameters() as $param) {
            $class = $param->getClass() ? $param->getClass()->name : null;
            $paramInfo[] = [$class, $param, isset($rule['substitutions']) && array_key_exists($class, $rule['substitutions'])];
        }

        // Return a closure that uses the cached information to generate the arguments for the method
        return function (array $args, array $share = []) use ($paramInfo, $rule) {
            // Now merge all the possible parameters: user-defined in the rule via constructParams, shared instances and the $args argument from $dice->create();
            if ($share || isset($rule['constructParams'])) $args = array_merge($args, isset($rule['constructParams']) ? $this->expand($rule['constructParams'], $share) : [], $share);

            $parameters = [];

            // Now find a value for each method parameter
            foreach ($paramInfo as list($class, $param, $sub)) {
                // First loop through $args and see whether or not each value can match the current parameter based on type hint
                if ($args) foreach ($args as $i => $arg) { // This if statement actually gives a ~10% speed increase when $args isn't set
                    if ($class && ($arg instanceof $class || ($arg === null && $param->allowsNull()))) {
                        // The argument matched, store it and remove it from $args so it won't wrongly match another parameter
                        $parameters[] = array_splice($args, $i, 1)[0];
                        // Move on to the next parameter
                        continue 2;
                    }
                }
                // When nothing from $args matches but a class is type hinted, create an instance to use, using a substitution if set
                if ($class) $parameters[] = $sub ? $this->expand($rule['substitutions'][$class], $share, true) : $this->create($class, [], $share);
                // There is no type hint, take the next available value from $args (and remove it from $args to stop it being reused)
                else if ($args) $parameters[] = $this->expand(array_shift($args));
                // For variadic parameters, provide remaining $args
                else if ($param->isVariadic()) $parameters = array_merge($parameters, $args);
                // There's no type hint and nothing left in $args, provide the default value or null
                else $parameters[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
            }
            // variadic functions will only have one argument. To account for those, append any remaining arguments to the list
            return $parameters;
        };
    }
}

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Without the change to addRule does the error still occur?

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

no

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

What error are you getting on line 135?

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

This error is caused by the call to create on line 135

Warning: Illegal offset type in isset or empty in C:\Users\Solle\OneDrive\Documents\Programing Stuff\Web development\serverRoot\elemukulek\lib\Dice\Dice.php on line 63

@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

Can you tweak this test to match your setup?

    public function testNamedInstanceCallWithInheritance() {
        $rule = [];
        $rule['call'] = [ 
                ['callMe', [1, 3] ],
                ['callMe', [3, 4] ]
        ];

        $this->dice->addRule('WithFunc', $rule);

        $rule = [];
        $rule['instanceOf'] = 'WithFunc';
        $rule['constructParams'] = ['Foo'];

        $this->dice->addRule('$MyInstance', $rule);

        $obj = $this->dice->create('$MyInstance');
        $this->assertEquals($obj->name, 'Foo');

    }

the withfunc class:

class WithFunc {
    public $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function callMe() {

    }
}

@solleer
Copy link
Contributor Author

solleer commented Jun 10, 2016

I figured out that the issue was caused because in order to work around the previous issue I set a maphper substitution twice and because of recursive it caused the substitution to become an array in a way not intended to work

TRPB added a commit that referenced this pull request Jun 10, 2016
@TRPB
Copy link
Member

TRPB commented Jun 10, 2016

I've added this feature and tests for it. There is a backwards compatibility break here but I think that's better than making the beavhiour of inherit inconsistent.

@solleer solleer closed this Jun 10, 2016
@solleer solleer deleted the patch-1 branch June 10, 2016 18:11
TRPB added a commit that referenced this pull request Jun 10, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants