Completed
Pull Request — master (#69)
by
unknown
04:20
created

CollectionIterator   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 205
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 14
c 0
b 0
f 0
lcom 1
cbo 6
dl 0
loc 205
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A get() 0 4 1
A parseRow() 0 7 1
A launchFilters() 0 12 3
A registerFilter() 0 13 2
A clearFilters() 0 6 1
A extract() 0 10 2
A slice() 0 4 1
A map() 0 4 1
A convertSlice() 0 12 1
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 Closure;
13
use PommProject\Foundation\ResultIterator;
14
use PommProject\Foundation\Session\ResultHandler;
15
use PommProject\Foundation\Session\Session;
16
use PommProject\ModelManager\Converter\PgEntity;
17
use PommProject\ModelManager\Exception\ModelException;
18
use PommProject\ModelManager\Model\FlexibleEntity\FlexibleEntityInterface;
19
20
/**
21
 * CollectionIterator
22
 *
23
 * Iterator for query results.
24
 *
25
 * @package   ModelManager
26
 * @copyright 2014 - 2015 Grégoire HUBERT
27
 * @author    Grégoire HUBERT <[email protected]>
28
 * @license   MIT/X11 {@link http://opensource.org/licenses/mit-license.php}
29
 */
30
class CollectionIterator extends ResultIterator
31
{
32
    /**
33
     * @var Session
34
     */
35
    protected $session;
36
37
    /**
38
     * @var Projection
39
     */
40
    protected $projection;
41
42
    /**
43
     * @var array
44
     */
45
    protected $filters = [];
46
47
    /**
48
     * @var HydrationPlan
49
     */
50
    protected $hydration_plan;
51
52
    /**
53
     * @var PgEntity
54
     */
55
    private $entity_converter;
56
57
    /**
58
     * __construct
59
     *
60
     * Constructor
61
     *
62
     * @access  public
63
     * @param   ResultHandler   $result
64
     * @param   Session         $session
65
     * @param   Projection      $projection
66
     */
67
    public function __construct(ResultHandler $result, Session $session, Projection $projection)
68
    {
69
        parent::__construct($result);
70
        $this->projection       = $projection;
71
        $this->session          = $session;
72
        $this->hydration_plan   = new HydrationPlan($projection, $session);
73
        $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...
74
          ->session
75
          ->getClientUsingPooler('converter', $this->projection->getFlexibleEntityClass())
76
          ->getConverter()
77
          ;
78
    }
79
80
    /**
81
     * get
82
     *
83
     * @see     ResultIterator
84
     * @return  FlexibleEntityInterface
85
     */
86
    public function get($index)
87
    {
88
        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...
89
    }
90
91
    /**
92
     * parseRow
93
     *
94
     * Convert values from Pg.
95
     *
96
     * @access  protected
97
     * @param   array          $values
98
     * @return  FlexibleEntityInterface
99
     * @see     ResultIterator
100
     */
101
    public function parseRow(array $values)
102
    {
103
        $values = $this->launchFilters($values);
104
        $entity = $this->hydration_plan->hydrate($values);
105
106
        return $this->entity_converter->cacheEntity($entity);
107
    }
108
109
    /**
110
     * launchFilters
111
     *
112
     * Launch filters on the given values.
113
     *
114
     * @access  protected
115
     * @param   array $values
116
     * @throws  ModelException   if return is not an array.
117
     * @return  array
118
     */
119
    protected function launchFilters(array $values)
120
    {
121
        foreach ($this->filters as $filter) {
122
            $values = call_user_func($filter, $values);
123
124
            if (!is_array($values)) {
125
                throw new ModelException(sprintf("Filter error. Filters MUST return an array of values."));
126
            }
127
        }
128
129
        return $values;
130
    }
131
132
    /**
133
     * registerFilter
134
     *
135
     * Register a new callable filter. All filters MUST return an associative
136
     * array with field name as key.
137
     *
138
     * @access public
139
     * @param  callable   $callable the filter.
140
     * @return CollectionIterator $this
141
     * @throws ModelException
142
     */
143
    public function registerFilter($callable)
144
    {
145
        if (!is_callable($callable)) {
146
            throw new ModelException(sprintf(
147
                "Given filter is not a callable (type '%s').",
148
                gettype($callable)
149
            ));
150
        }
151
152
        $this->filters[] = $callable;
153
154
        return $this;
155
    }
156
157
    /**
158
     * clearFilters
159
     *
160
     * Empty the filter stack.
161
     */
162
    public function clearFilters()
163
    {
164
        $this->filters = [];
165
166
        return $this;
167
    }
168
169
    /**
170
     * extract
171
     *
172
     * Return an array of entities extracted as arrays.
173
     *
174
     * @access public
175
     * @return array
176
     */
177
    public function extract()
178
    {
179
        $results = [];
180
181
        foreach ($this as $result) {
182
            $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...
183
        }
184
185
        return $results;
186
    }
187
188
    /**
189
     * slice
190
     *
191
     * see @ResultIterator
192
     *
193
     * @access public
194
     * @param  string   $name
195
     * @return array
196
     */
197
    public function slice($name)
198
    {
199
        return $this->convertSlice(parent::slice($name), $name);
200
    }
201
202
    /**
203
     * @param Closure $callback
204
     * @return array
205
     */
206
    public function map(Closure $callback)
207
    {
208
        return array_map($callback, iterator_to_array($this));
209
    }
210
211
212
    /**
213
     * convertSlice
214
     *
215
     * Convert a slice.
216
     *
217
     * @access protected
218
     * @param  array  $values
219
     * @param  string $name
220
     * @return array
221
     */
222
    protected function convertSlice(array $values, $name)
223
    {
224
        $type = $this->projection->getFieldType($name);
225
        $converter = $this->hydration_plan->getConverterForField($name);
226
227
        return array_map(
228
            function ($val) use ($converter, $type) {
229
                return $converter->fromPg($val, $type, $this->session);
230
            },
231
            $values
232
        );
233
    }
234
}
235