Completed
Pull Request — master (#69)
by
unknown
01:45
created

CollectionIterator::convertSlice()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
/*
3
 * This file is part of the PommProject's ModelManager package.
4
 *
5
 * (c) 2014 - 2015 Grégoire HUBERT <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace PommProject\ModelManager\Model;
11
12
use PommProject\Foundation\ResultIterator;
13
use PommProject\Foundation\Session\ResultHandler;
14
use PommProject\Foundation\Session\Session;
15
use PommProject\ModelManager\Converter\PgEntity;
16
use PommProject\ModelManager\Exception\ModelException;
17
use PommProject\ModelManager\Model\FlexibleEntity\FlexibleEntityInterface;
18
19
/**
20
 * CollectionIterator
21
 *
22
 * Iterator for query results.
23
 *
24
 * @package   ModelManager
25
 * @copyright 2014 - 2015 Grégoire HUBERT
26
 * @author    Grégoire HUBERT <[email protected]>
27
 * @license   MIT/X11 {@link http://opensource.org/licenses/mit-license.php}
28
 */
29
class CollectionIterator extends ResultIterator
30
{
31
    /**
32
     * @var Session
33
     */
34
    protected $session;
35
36
    /**
37
     * @var Projection
38
     */
39
    protected $projection;
40
41
    /**
42
     * @var array
43
     */
44
    protected $filters = [];
45
46
    /**
47
     * @var HydrationPlan
48
     */
49
    protected $hydration_plan;
50
51
    /**
52
     * @var PgEntity
53
     */
54
    private $entity_converter;
55
56
    /**
57
     * __construct
58
     *
59
     * Constructor
60
     *
61
     * @access  public
62
     * @param   ResultHandler   $result
63
     * @param   Session         $session
64
     * @param   Projection      $projection
65
     */
66
    public function __construct(ResultHandler $result, Session $session, Projection $projection)
67
    {
68
        parent::__construct($result);
69
        $this->projection       = $projection;
70
        $this->session          = $session;
71
        $this->hydration_plan   = new HydrationPlan($projection, $session);
72
        $this->entity_converter = $this
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PommProject\Foundation\Client\ClientInterface as the method getConverter() does only exist in the following implementations of said interface: PommProject\Foundation\Converter\ConverterClient.

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...
73
          ->session
74
          ->getClientUsingPooler('converter', $this->projection->getFlexibleEntityClass())
75
          ->getConverter()
76
          ;
77
    }
78
79
    /**
80
     * get
81
     *
82
     * @see     ResultIterator
83
     * @return  FlexibleEntityInterface
84
     */
85
    public function get($index)
86
    {
87
        return $this->parseRow(parent::get($index));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->parseRow(parent::get($index)); (PommProject\ModelManager...FlexibleEntityInterface) is incompatible with the return type of the parent method PommProject\Foundation\ResultIterator::get of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
88
    }
89
90
    /**
91
     * parseRow
92
     *
93
     * Convert values from Pg.
94
     *
95
     * @access  protected
96
     * @param   array          $values
97
     * @return  FlexibleEntityInterface
98
     * @see     ResultIterator
99
     */
100
    public function parseRow(array $values)
101
    {
102
        $values = $this->launchFilters($values);
103
        $entity = $this->hydration_plan->hydrate($values);
104
105
        return $this->entity_converter->cacheEntity($entity);
106
    }
107
108
    /**
109
     * launchFilters
110
     *
111
     * Launch filters on the given values.
112
     *
113
     * @access  protected
114
     * @param   array $values
115
     * @throws  ModelException   if return is not an array.
116
     * @return  array
117
     */
118
    protected function launchFilters(array $values)
119
    {
120
        foreach ($this->filters as $filter) {
121
            $values = call_user_func($filter, $values);
122
123
            if (!is_array($values)) {
124
                throw new ModelException(sprintf("Filter error. Filters MUST return an array of values."));
125
            }
126
        }
127
128
        return $values;
129
    }
130
131
    /**
132
     * registerFilter
133
     *
134
     * Register a new callable filter. All filters MUST return an associative
135
     * array with field name as key.
136
     *
137
     * @access public
138
     * @param  callable   $callable the filter.
139
     * @return CollectionIterator $this
140
     * @throws ModelException
141
     */
142
    public function registerFilter($callable)
143
    {
144
        if (!is_callable($callable)) {
145
            throw new ModelException(sprintf(
146
                "Given filter is not a callable (type '%s').",
147
                gettype($callable)
148
            ));
149
        }
150
151
        $this->filters[] = $callable;
152
153
        return $this;
154
    }
155
156
    /**
157
     * clearFilters
158
     *
159
     * Empty the filter stack.
160
     */
161
    public function clearFilters()
162
    {
163
        $this->filters = [];
164
165
        return $this;
166
    }
167
168
    /**
169
     * extract
170
     *
171
     * Return an array of entities extracted as arrays.
172
     *
173
     * @access public
174
     * @return array
175
     */
176
    public function extract()
177
    {
178
        $results = [];
179
180
        foreach ($this as $result) {
181
            $results[] = $result->extract();
0 ignored issues
show
Bug introduced by
The method extract cannot be called on $result (of type array|null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
182
        }
183
184
        return $results;
185
    }
186
187
    /**
188
     * slice
189
     *
190
     * see @ResultIterator
191
     *
192
     * @access public
193
     * @param  string   $name
194
     * @return array
195
     */
196
    public function slice($name)
197
    {
198
        return $this->convertSlice(parent::slice($name), $name);
199
    }
200
201
    /**
202
     * @return array
203
     */
204
    public function map(\Closure $callback)
205
    {
206
        return array_map($callback, iterator_to_array($this));
207
    }
208
209
    /**
210
     * convertSlice
211
     *
212
     * Convert a slice.
213
     *
214
     * @access protected
215
     * @param  array  $values
216
     * @param  string $name
217
     * @return array
218
     */
219
    protected function convertSlice(array $values, $name)
220
    {
221
        $type = $this->projection->getFieldType($name);
222
        $converter = $this->hydration_plan->getConverterForField($name);
223
224
        return array_map(
225
            function ($val) use ($converter, $type) {
226
                return $converter->fromPg($val, $type, $this->session);
227
            },
228
            $values
229
        );
230
    }
231
}
232