Completed
Pull Request — master (#263)
by Kévin
06:01 queued 03:06
created

MagicCallPatch::supports()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Prophecy.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *     Marcello Duarte <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Prophecy\Doubler\ClassPatch;
13
14
use phpDocumentor\Reflection\DocBlock\Tags\Method;
15
use phpDocumentor\Reflection\DocBlockFactory;
16
use phpDocumentor\Reflection\DocBlockFactoryInterface;
17
use phpDocumentor\Reflection\Types\ContextFactory;
18
use Prophecy\Doubler\Generator\Node\ClassNode;
19
use Prophecy\Doubler\Generator\Node\MethodNode;
20
21
/**
22
 * Discover Magical API using "@method" PHPDoc format.
23
 *
24
 * @author Thomas Tourlourat <[email protected]>
25
 * @author Kévin Dunglas <[email protected]>
26
 */
27
class MagicCallPatch implements ClassPatchInterface
28
{
29
    private $docBlockFactory;
30
    private $contextFactory;
31
32
    public function __construct()
33
    {
34
        $this->docBlockFactory = DocBlockFactory::createInstance();
35
        $this->contextFactory = new ContextFactory();
36
    }
37
38
    /**
39
     * Support any class
40
     *
41
     * @param ClassNode $node
42
     *
43
     * @return boolean
44
     */
45
    public function supports(ClassNode $node)
46
    {
47
        return true;
48
    }
49
50
    /**
51
     * Discover Magical API
52
     *
53
     * @param ClassNode $node
54
     */
55
    public function apply(ClassNode $node)
56
    {
57
        $parentClass = $node->getParentClass();
58
        $reflectionClass = new \ReflectionClass($parentClass);
59
60
        try {
61
            $phpdoc = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass));
62
        } catch (\InvalidArgumentException $e) {
63
            // No DocBlock
64
        }
65
66
        /**
67
         * @var Method[] $tagList
68
         */
69
        $tagList = isset($phpdoc) ? $phpdoc->getTagsByName('method') : array();
70
71
        $interfaces = $reflectionClass->getInterfaces();
72
        foreach($interfaces as $interface) {
73
            try {
74
                $phpdoc = $this->docBlockFactory->create($interface, $this->contextFactory->createFromReflector($interface));
75
                $tagList = array_merge($tagList, $phpdoc->getTagsByName('method'));
76
            } catch (\InvalidArgumentException $e) {
77
                // No DocBlock
78
            }
79
        }
80
81
        foreach($tagList as $tag) {
82
            $methodName = $tag->getMethodName();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface phpDocumentor\Reflection\DocBlock\Tag as the method getMethodName() does only exist in the following implementations of said interface: phpDocumentor\Reflection\DocBlock\Tags\Method.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
83
84
            if (empty($methodName)) {
85
                continue;
86
            }
87
88
            if (!$reflectionClass->hasMethod($methodName)) {
89
                $methodNode = new MethodNode($tag->getMethodName());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface phpDocumentor\Reflection\DocBlock\Tag as the method getMethodName() does only exist in the following implementations of said interface: phpDocumentor\Reflection\DocBlock\Tags\Method.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
90
                $methodNode->setStatic($tag->isStatic());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface phpDocumentor\Reflection\DocBlock\Tag as the method isStatic() does only exist in the following implementations of said interface: phpDocumentor\Reflection\DocBlock\Tags\Method.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
91
92
                $node->addMethod($methodNode);
93
            }
94
        }
95
    }
96
97
    /**
98
     * Returns patch priority, which determines when patch will be applied.
99
     *
100
     * @return integer Priority number (higher - earlier)
101
     */
102
    public function getPriority()
103
    {
104
        return 50;
105
    }
106
}
107
108