Completed
Pull Request — master (#162)
by Tim
15:26 queued 05:36
created

UrlKeyObserver::getPrimaryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Product\Observers\UrlKeyObserver
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2016 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import-product
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Product\Observers;
22
23
use Zend\Filter\FilterInterface;
24
use TechDivision\Import\Utils\StoreViewCodes;
25
use TechDivision\Import\Utils\UrlKeyUtilInterface;
26
use TechDivision\Import\Utils\Filter\UrlKeyFilterTrait;
27
use TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface;
28
use TechDivision\Import\Product\Utils\MemberNames;
29
use TechDivision\Import\Product\Utils\ColumnKeys;
30
use TechDivision\Import\Product\Utils\ConfigurationKeys;
31
use TechDivision\Import\Product\Services\ProductBunchProcessorInterface;
32
33
/**
34
 * Observer that extracts the URL key from the product name and adds a two new columns
35
 * with the their values.
36
 *
37
 * @author    Tim Wagner <[email protected]>
38
 * @copyright 2016 TechDivision GmbH <[email protected]>
39
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
40
 * @link      https://github.com/techdivision/import-product
41
 * @link      http://www.techdivision.com
42
 */
43
class UrlKeyObserver extends AbstractProductImportObserver
44
{
45
46
    /**
47
     * The trait that provides string => URL key conversion functionality.
48
     *
49
     * @var \TechDivision\Import\Utils\Filter\UrlKeyFilterTrait
50
     */
51
    use UrlKeyFilterTrait;
52
53
    /**
54
     * The URL key utility instance.
55
     *
56
     * @var \TechDivision\Import\Utils\UrlKeyUtilInterface
57
     */
58
    protected $urlKeyUtil;
59
60
    /**
61
     * The product bunch processor instance.
62
     *
63
     * @var \TechDivision\Import\Product\Services\ProductBunchProcessorInterface
64
     */
65
    protected $productBunchProcessor;
66
67
    /**
68
     * Initialize the observer with the passed product bunch processor and filter instance.
69
     *
70
     * @param \TechDivision\Import\Product\Services\ProductBunchProcessorInterface $productBunchProcessor   The product bunch processor instance
71
     * @param \Zend\Filter\FilterInterface                                         $convertLiteralUrlFilter The URL filter instance
72
     * @param \TechDivision\Import\Utils\UrlKeyUtilInterface                       $urlKeyUtil              The URL key utility instance
73
     */
74
    public function __construct(
75
        ProductBunchProcessorInterface $productBunchProcessor,
76
        FilterInterface $convertLiteralUrlFilter,
77
        UrlKeyUtilInterface $urlKeyUtil
78
    ) {
79
        $this->productBunchProcessor = $productBunchProcessor;
80
        $this->convertLiteralUrlFilter = $convertLiteralUrlFilter;
81
        $this->urlKeyUtil = $urlKeyUtil;
82
    }
83
84
    /**
85
     * Return's the product bunch processor instance.
86
     *
87
     * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance
88
     */
89
    protected function getProductBunchProcessor() : ProductBunchProcessorInterface
90
    {
91
        return $this->productBunchProcessor;
92
    }
93
94
    /**
95
     * Returns the URL key utility instance.
96
     *
97
     * @return \TechDivision\Import\Utils\UrlKeyUtilInterface The URL key utility instance
98
     */
99
    protected function getUrlKeyUtil() : UrlKeyUtilInterface
100
    {
101
        return $this->urlKeyUtil;
102
    }
103
104
    /**
105
     * Process the observer's business logic.
106
     *
107
     * @return void
108
     * @throws \Exception Is thrown, if either column "url_key" or "name" have a value set
109
     */
110
    protected function process()
111
    {
112
113
        // initialize the URL key and the product
114
        $urlKey = null;
115
        $product = array();
0 ignored issues
show
Unused Code introduced by
$product is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
116
117
        // prepare the store view code
118
        $this->getSubject()->prepareStoreViewCode();
119
120
        // set the entity ID for the product with the passed SKU
121
        if ($product = $this->loadProduct($sku = $this->getValue(ColumnKeys::SKU))) {
122
            $this->setIds($product);
123
        } else {
124
            $this->setIds(array());
125
        }
126
127
        // query whether or not the URL key column has a value
128
        if ($this->hasValue(ColumnKeys::URL_KEY)) {
129
            $urlKey = $this->getValue(ColumnKeys::URL_KEY);
130
        } else {
131
            // query whether or not the existing product `url_key` should be re-created from the product name
132
            if ($product && !$this->getSubject()->getConfiguration()->getParam(ConfigurationKeys::UPDATE_URL_KEY_FROM_NAME, true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $product of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
133
                // if the product already exists and NO re-creation from the product name has to
134
                // be done, load the original `url_key`from the product and use that to proceed
135
                $urlKey = $this->loadUrlKey($this->getSubject(), $this->getPrimaryKey());
0 ignored issues
show
Documentation introduced by
$this->getSubject() is of type object<TechDivision\Impo...jects\SubjectInterface>, but the function expects a object<TechDivision\Impo...yAwareSubjectInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
136
            }
137
138
            // try to load the value from column `name` if URL key is still
139
            // empty, because we need it to process the the rewrites later on
140
            if ($urlKey === null || $urlKey === '' && $this->hasValue(ColumnKeys::NAME)) {
141
                $urlKey = $this->convertNameToUrlKey($this->getValue(ColumnKeys::NAME));
142
            }
143
        }
144
145
        // stop processing, if no URL key is available
146
        if ($urlKey === null || $urlKey === '') {
147
            // throw an exception, that the URL key can not be
148
            // initialized and we're in the default store view
149
            if ($this->getStoreViewCode(StoreViewCodes::ADMIN) === StoreViewCodes::ADMIN) {
150
                throw new \Exception(sprintf('Can\'t initialize the URL key for product "%s" because columns "url_key" or "name" have a value set for default store view', $sku));
151
            }
152
            // stop processing, because we're in a store
153
            // view row and a URL key is not mandatory
154
            return;
155
        }
156
157
        // set the unique URL key for further processing
158
        $this->setValue(ColumnKeys::URL_KEY, $this->makeUnique($this->getSubject(), $urlKey, $this->getUrlPaths()));
0 ignored issues
show
Documentation introduced by
$this->getSubject() is of type object<TechDivision\Impo...jects\SubjectInterface>, but the function expects a object<TechDivision\Impo...yAwareSubjectInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
159
    }
160
161
    /**
162
     * Extract's the category from the comma separeted list of categories
163
     * in column `categories` and return's an array with their URL paths.
164
     *
165
     * @return string[] Array with the URL paths of the categories found in column `categories`
166
     */
167
    protected function getUrlPaths()
168
    {
169
170
        // initialize the array for the URL paths of the cateogries
171
        $urlPaths = array();
172
173
        // extract the categories from the column `categories`
174
        $categories = $this->getValue(ColumnKeys::CATEGORIES, array(), array($this, 'explode'));
175
176
        // the URL paths are store view specific, so we need
177
        // the store view code to load the appropriate ones
178
        $storeViewCode = $this->getStoreViewCode(StoreViewCodes::ADMIN);
179
180
        // iterate of the found categories, load their URL path as well as the URL path of
181
        // parent categories, if they have the anchor flag activated and add it the array
182
        foreach ($categories as $path) {
183
            // load the category based on the category path
184
            $category = $this->getCategoryByPath($path, $storeViewCode);
185
            // try to resolve the URL paths recursively
186
            $this->resolveUrlPaths($urlPaths, $category, $storeViewCode);
187
        }
188
189
        // return the array with the recursively resolved URL paths
190
        // of the categories that are related with the products
191
        return $urlPaths;
192
    }
193
194
    /**
195
     * Recursively resolves an array with the store view specific
196
     * URL paths of the passed category.
197
     *
198
     * @param array  $urlPaths       The array to append the URL paths to
199
     * @param array  $category       The category to resolve the list with URL paths
200
     * @param string $storeViewCode  The store view code to resolve the URL paths for
201
     * @param bool   $directRelation the flag whether or not the passed category is a direct relation to the product and has to added to the list
202
     *
203
     * @return void
204
     */
205
    protected function resolveUrlPaths(array &$urlPaths, array $category, string $storeViewCode, bool $directRelation = true)
206
    {
207
208
        // load the root category
209
        $rootCategory = $this->getRootCategory();
210
211
        // try to resolve the parent category IDs, but only if either
212
        // the actual category nor it's parent is a root category
213
        if ($rootCategory[MemberNames::ENTITY_ID] !== $category[MemberNames::ENTITY_ID] &&
214
            $rootCategory[MemberNames::ENTITY_ID] !== $category[MemberNames::PARENT_ID]
215
        ) {
216
            // load the parent category
217
            $parent = $this->getCategory($category[MemberNames::PARENT_ID], $storeViewCode);
218
            // also resolve the URL paths for the parent category
219
            $this->resolveUrlPaths($urlPaths, $parent, $storeViewCode, false);
220
        }
221
222
        // query whether or not the URL path already
223
        // is part of the list (to avoid duplicates)
224
        if (in_array($category[MemberNames::URL_PATH], $urlPaths)) {
225
            return;
226
        }
227
228
        // append the URL path, either if we've a direct relation
229
        // or the category has the anchor flag actvated
230
        if ($directRelation === true || ($category[MemberNames::IS_ANCHOR] === 1 && $directRelation === false)) {
231
            $urlPaths[] = $category[MemberNames::URL_PATH];
232
        }
233
    }
234
235
    /**
236
     * Temporarily persist's the IDs of the passed product.
237
     *
238
     * @param array $product The product to temporarily persist the IDs for
239
     *
240
     * @return void
241
     */
242
    protected function setIds(array $product)
243
    {
244
        $this->setLastEntityId(isset($product[MemberNames::ENTITY_ID]) ? $product[MemberNames::ENTITY_ID] : null);
245
    }
246
247
    /**
248
     * Set's the ID of the product that has been created recently.
249
     *
250
     * @param string $lastEntityId The entity ID
251
     *
252
     * @return void
253
     */
254
    protected function setLastEntityId($lastEntityId)
255
    {
256
        $this->getSubject()->setLastEntityId($lastEntityId);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method setLastEntityId() does only exist in the following implementations of said interface: TechDivision\Import\Plugins\ExportableSubjectImpl, TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Subjects\ExportableTraitImpl.

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...
257
    }
258
259
    /**
260
     * Return's the PK to of the product.
261
     *
262
     * @return integer The PK to create the relation with
263
     */
264
    protected function getPrimaryKey()
265
    {
266
        $this->getSubject()->getLastEntityId();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getLastEntityId() does only exist in the following implementations of said interface: TechDivision\Import\Observers\EntitySubjectImpl, TechDivision\Import\Plugins\ExportableSubjectImpl, TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Subjects\ExportableTraitImpl.

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...
267
    }
268
269
    /**
270
     * Load's and return's the product with the passed SKU.
271
     *
272
     * @param string $sku The SKU of the product to load
273
     *
274
     * @return array The product
275
     */
276
    protected function loadProduct($sku)
277
    {
278
        return $this->getProductBunchProcessor()->loadProduct($sku);
279
    }
280
281
    /**
282
     * Load's and return's the url_key with the passed primary ID.
283
     *
284
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject      The subject to load the URL key
285
     * @param int                                                       $primaryKeyId The ID from product
286
     *
287
     * @return string|null url_key or null
288
     */
289
    protected function loadUrlKey(UrlKeyAwareSubjectInterface $subject, $primaryKeyId)
290
    {
291
        return $this->getUrlKeyUtil()->loadUrlKey($subject, $primaryKeyId);
0 ignored issues
show
Bug introduced by
The method loadUrlKey() does not seem to exist on object<TechDivision\Impo...ls\UrlKeyUtilInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
292
    }
293
294
    /**
295
     * Return's the category with the passed path.
296
     *
297
     * @param string $path          The path of the category to return
298
     * @param string $storeViewCode The code of a store view, defaults to admin
299
     *
300
     * @return array The category
301
     */
302
    protected function getCategoryByPath($path, $storeViewCode = StoreViewCodes::ADMIN)
0 ignored issues
show
Unused Code introduced by
The parameter $storeViewCode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
303
    {
304
        return $this->getSubject()->getCategoryByPath($path);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getCategoryByPath() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject.

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...
305
    }
306
307
    /**
308
     * Make's the passed URL key unique by adding the next number to the end.
309
     *
310
     * @param \TechDivision\Import\Subjects\UrlKeyAwareSubjectInterface $subject  The subject to make the URL key unique for
311
     * @param string                                                    $urlKey   The URL key to make unique
312
     * @param array                                                     $urlPaths The URL paths to make unique
313
     *
314
     * @return string The unique URL key
315
     */
316
    protected function makeUnique(UrlKeyAwareSubjectInterface $subject, string $urlKey, array $urlPaths = array())
317
    {
318
        return $this->getUrlKeyUtil()->makeUnique($subject, $urlKey, $urlPaths);
0 ignored issues
show
Unused Code introduced by
The call to UrlKeyUtilInterface::makeUnique() has too many arguments starting with $urlPaths.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
319
    }
320
321
    /**
322
     * Return's the root category for the actual view store.
323
     *
324
     * @return array The store's root category
325
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
326
     */
327
    protected function getRootCategory()
328
    {
329
        return $this->getSubject()->getRootCategory();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getRootCategory() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject, TechDivision\Import\Subjects\AbstractEavSubject, TechDivision\Import\Subjects\AbstractSubject, TechDivision\Import\Subjects\MoveFilesSubject, TechDivision\Import\Subjects\ValidatorSubject.

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...
330
    }
331
332
    /**
333
     * Return's the category with the passed ID.
334
     *
335
     * @param integer $categoryId    The ID of the category to return
336
     * @param string  $storeViewCode The code of a store view, defaults to "admin"
337
     *
338
     * @return array The category data
339
     */
340
    protected function getCategory($categoryId, $storeViewCode = StoreViewCodes::ADMIN)
341
    {
342
        return $this->getSubject()->getCategory($categoryId, $storeViewCode);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Subjects\SubjectInterface as the method getCategory() does only exist in the following implementations of said interface: TechDivision\Import\Prod...\AbstractProductSubject, TechDivision\Import\Product\Subjects\BunchSubject.

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...
343
    }
344
}
345