Completed
Pull Request — master (#16)
by Hannes
05:53
created

Collection::merge()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 8
ccs 0
cts 0
cp 0
rs 9.4286
cc 2
eloc 4
nc 2
nop 1
crap 6
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Baleen\Migrations\Version;
21
22
use Baleen\Migrations\Exception\InvalidArgumentException;
23
use Baleen\Migrations\Exception\Version\Collection\AlreadyExistsException;
24
use Baleen\Migrations\Exception\Version\Collection\CollectionException;
25
use Baleen\Migrations\Version;
26
use Baleen\Migrations\Version\Collection\Resolver\DefaultResolverStackFactory;
27
use Baleen\Migrations\Version\Collection\Resolver\ResolverInterface;
28
use Doctrine\Common\Collections\ArrayCollection;
29
use Zend\Stdlib\ArrayUtils;
30
31
/**
32
 * Class Collection.
33
 *
34
 * @author Gabriel Somoza <[email protected]>
35
 *
36
 * IMPROVE: this class has many methods. Consider refactoring it to keep number of methods under 10.
37
 *
38
 * @SuppressWarnings(PHPMD.TooManyMethods)
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 *
41
 * @method VersionInterface first()
42
 * @method VersionInterface last()
43
 * @method VersionInterface next()
44
 * @method VersionInterface current()
45
 * @method VersionInterface offsetGet($offset)
46
 * @method VersionInterface offsetUnset($offset)
47
 * @method VersionInterface[] toArray()
48
 * @method VersionInterface[] getValues()
49
 * @property VersionInterface[] elements
50
 */
51
class Collection extends ArrayCollection
52
{
53
    /** @var ResolverInterface */
54
    private $resolver;
55
56
    /**
57
     * @param VersionInterface[]|\Traversable $versions
58
     * @param ResolverInterface $resolver
0 ignored issues
show
Documentation introduced by
Should the type for parameter $resolver not be null|ResolverInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
59
     *
60
     * @throws AlreadyExistsException
61
     * @throws InvalidArgumentException
62
     * @throws CollectionException
63
     */
64 156
    public function __construct(
65
        $versions = array(),
66
        ResolverInterface $resolver = null
67
    ) {
68 156
        if (!is_array($versions)) {
69 11
            if ($versions instanceof \Traversable) {
70 4
                $versions = ArrayUtils::iteratorToArray($versions);
71 4
            } else {
72 7
                throw new InvalidArgumentException(
73
                    "Constructor parameter 'versions' must be an array or Traversable object."
74 7
                );
75
            }
76 4
        }
77
78 151
        if (null === $resolver) {
79 114
            $resolver = DefaultResolverStackFactory::create();
80 114
        }
81 151
        $this->resolver = $resolver;
82
83 151
        foreach ($versions as $version) {
84 133
            $this->validate($version);
85 151
        }
86
87 151
        parent::__construct($versions);
88 151
    }
89
90
    /**
91
     * @return ResolverInterface
92
     */
93 118
    final protected function getResolver()
94
    {
95 118
        return $this->resolver;
96
    }
97
98
    /**
99
     * Gets an element.
100
     *
101
     * @param mixed $key If an alias is given then it will be resolved to an element. Otherwise the $key will be used
102
     *                   to fetch the element by index.
103
     * @param bool $resolve Whether to use the resolver or not.
104
     *
105
     * @return VersionInterface|null Null if not present
106
     */
107 98
    public function get($key, $resolve = true)
108
    {
109 98
        $result = null;
110
111 98
        if (is_object($key) && $key instanceof VersionInterface) {
112 1
            return $this->getById($key->getId());
113
        }
114
115 97
        if ($resolve && is_string($key)) {
116 88
            $result = $this->resolve($key);
117 88
        }
118
119 97
        if (null === $result && is_scalar($key)) {
120 79
            $result = parent::get($key);
121 79
        }
122
123 97
        return $result;
124
    }
125
126
    /**
127
     * Gets a version by id
128
     *
129
     * @param $id
130
     *
131
     * @return VersionInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be VersionInterface|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
132
     */
133 44
    public function getById($id)
134
    {
135 44
        $index = $this->indexOfId($id);
136 44
        return $this->get($index, false);
137
    }
138
139
    /**
140
     * Returns the index of the version that has the given id. Returns null if not found.
141
     *
142
     * @param string $id
143
     *
144
     * @return int|null
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
145
     */
146 68
    public function indexOfId($id)
147
    {
148 68
        $id = (string) $id;
149 68
        $result = null;
150 68
        $lazy = true;
151 68
        foreach ($this as $index => $version) {
152
            // perfect match
153 68
            $currentId = $version->getId();
154 68
            if ($currentId === $id) {
155 50
                $result = $index;
156 50
                break;
157
            }
158
159
            // lazy match:
160 54
            if ($lazy && strlen($id) < strlen($currentId) && substr($currentId, 0, strlen($id)) === $id) {
161 8
                if ($result === null) {
162
                    // make this version a candidate, but continue searching to see if any other items also meet the
163
                    // condition (in which case we'd know the $id being searched for is "ambiguous")
164 8
                    $result = $index;
165 8
                } else {
166
                    // the $id is ambiguous when used for lazy matching
167 4
                    $lazy = false; // abort lazy search, match by exact ID from now on
168 4
                    $result = null; // remove the candidate
169
                }
170 8
            }
171 68
        }
172 68
        return $result;
173
    }
174
175
    /**
176
     * Resolves an alias in to a version
177
     *
178
     * @param string $alias
179
     *
180
     * @return VersionInterface|null
181
     */
182 88
    protected function resolve($alias)
183
    {
184 88
        return $this->getResolver()->resolve((string) $alias, $this);
185
    }
186
187
    /**
188
     * Returns whether the key exists in the collection.
189
     *
190
     * @param      $index
191
     * @param bool $resolve
192
     *
193
     * @return bool
194
     */
195 8
    public function has($index, $resolve = true)
196
    {
197 8
        return $this->get($index, $resolve) !== null;
198
    }
199
200
    /**
201
     * Returns true if the specified version is valid (can be added) to the collection. Otherwise, it MUST throw
202
     * an exception.
203
     *
204
     * @param VersionInterface $version
205
     *
206
     * @return bool
207
     *
208
     * @throws AlreadyExistsException
209
     * @throws CollectionException
210
     */
211 141
    public function validate(VersionInterface $version)
212
    {
213
        // validate the version can be added to the collection
214 141
        if (!$this->isEmpty() && $this->contains($version)) {
215 5
            throw new AlreadyExistsException(
216 5
                sprintf('Item with id "%s" already exists.', $version->getId())
217 5
            );
218
        }
219
220 139
        return true; // if there are no exceptions then result is true
221
    }
222
223
    /**
224
     * invalidateResolverCache
225
     */
226 42
    protected function invalidateResolverCache()
227
    {
228 42
        $this->getResolver()->clearCache($this);
229 42
    }
230
231
    /**
232
     * Add a version to the collection
233
     *
234
     * @param mixed $element
235
     *
236
     * @return bool
237
     *
238
     * @throws CollectionException
239
     * @throws InvalidArgumentException
240
     */
241 42
    public function add($element)
242
    {
243 42 View Code Duplication
        if (!$element instanceof VersionInterface) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244 1
            throw new InvalidArgumentException(sprintf(
245 1
                'Invalid type "%s". Can only add instances of "%s" to this collection.',
246
                VersionInterface::class,
247
                is_object($element) ? get_class($element) : gettype($element)
248
            ));
249
        }
250
251
        if ($this->validate($element)) {
252
            parent::add($element);
253
            $this->invalidateResolverCache();
254
        } else {
255
            // this should never happen
256
            throw new CollectionException(
257
                'Validate should either return true or throw an exception'
258
            );
259
        }
260
        return true;
261
    }
262
263
    /**
264
     * @param $key
265
     *
266
     * @return VersionInterface|null
267
     */
268
    public function remove($key)
269
    {
270
        if (is_object($key)) {
271
            $result = $this->removeElement($key);
272
        } else {
273
            $result = parent::remove($key);
274
        }
275
276
        if (null !== $result) {
277
            $this->invalidateResolverCache();
278
        }
279
        return $result;
280
    }
281
282
    /**
283
     * Adds a new version to the collection if it doesn't exist or replaces it if it does.
284
     *
285
     * @param VersionInterface $version
286
     */
287
    public function addOrReplace(VersionInterface $version)
288
    {
289
        $index = $this->indexOfId($version->getId());
290
        if (null !== $index) {
291
            $this->remove($index);
292
        }
293
        $this->add($version);
294
    }
295
296
    /**
297
     * Returns a new collection with "enriched" elements based on the information provided in the parameter.
298
     * An "enriched" Version is one that was originally not linked and now is linked, not migrated and now is migrated,
299
     * or both.
300
     *
301
     * @param Collection $versions
302
     *
303
     * @return static
304
     *
305
     * @throws CollectionException
306
     * @throws InvalidArgumentException
307
     */
308
    public function hydrate(Collection $versions)
309
    {
310
        foreach ($versions as $update) {
311
            $current = $this->getById($update->getId());
312
            if ($current !== null) {
313
                $current->setMigrated($update->isMigrated());
314
                if ($update instanceof LinkedVersion && $current instanceof Version) {
315
                    $key = $this->indexOf($current);
316
                    $current = $current->withMigration($update->getMigration());
317
                    $this->set($key, $current);
318
                }
319
                try {
320
                    $this->validate($current);
321
                } catch (AlreadyExistsException $e) {
322
                    // we don't care for this validation, but yes for the rest
323
                }
324
            }
325
        }
326
        return $this;
327
    }
328
329
    /**
330
     * Merges another set into this one, replacing versions that exist and adding those that don't.
331
     *
332
     * @param Collection $collection
333
     * @return $this
334
     */
335
    public function merge(Collection $collection)
336
    {
337
        foreach ($collection as $version) {
338
            $this->addOrReplace($version);
339
        }
340
341
        return $this;
342
    }
343
}
344