Passed
Push — master ( 6bb734...c9a16f )
by Oliver
08:15
created

MetaItemCollection   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 99.01%

Importance

Changes 10
Bugs 2 Features 2
Metric Value
wmc 36
c 10
b 2
f 2
lcom 2
cbo 2
dl 0
loc 299
ccs 100
cts 101
cp 0.9901
rs 8.8

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A setTags() 0 10 3
A modelKeys() 0 12 3
A originalModelKeys() 0 4 1
A add() 0 18 4
A findItem() 0 12 3
A observeDeletions() 0 8 3
A observeDeletion() 0 10 2
A getMetaItemClass() 0 4 1
A setMetaItemClass() 0 8 2
A getDefaultTag() 0 4 1
A setDefaultTag() 0 6 1
A __call() 0 8 3
A __isset() 0 4 1
A __unset() 0 8 2
A __get() 0 14 3
A __set() 0 18 2
1
<?php
2
3
/*
4
 * This file is part of Mailable.
5
 *
6
 * (c) Oliver Green <[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 BoxedCode\Eloquent\Meta;
13
14
use Illuminate\Database\Eloquent\Collection as CollectionBase;
15
use BoxedCode\Eloquent\Meta\Contracts\MetaItem as MetaItemContract;
16
use BoxedCode\Eloquent\Meta\Contracts\MetaItemCollection as CollectionContract;
17
use InvalidArgumentException;
18
19
class MetaItemCollection extends CollectionBase implements CollectionContract
20
{
21
    /**
22
     * Fully qualified class name to use when creating new items via magic methods.
23
     *
24
     * @var string
25
     */
26
    protected static $item_class;
27
28
    /**
29
     * The default tag name to use when using magic methods.
30
     *
31
     * @var string
32
     */
33
    protected $default_tag = 'default';
34
35
    /**
36
     * Keys of the models that the collection was constructed with.
37
     *
38
     * @var array
39
     */
40
    protected $original_model_keys = [];
41
42
    /**
43
     * MetaItemCollection constructor.
44
     *
45
     * @param array $items
46
     */
47 29
    public function __construct($items = [])
48
    {
49 29
        parent::__construct($items);
50
51 29
        $this->original_model_keys = $this->modelKeys();
52
53 29
        $this->setTags($this->items);
54
55 29
        $this->observeDeletions($this->items);
56 29
    }
57
58
    /**
59
     * Sets the default tag on any 'tag-less' items.
60
     *
61
     * @param array $items
62
     */
63 29
    protected function setTags(array $items)
64
    {
65
        array_map(function ($item) {
66 23
            if ($item instanceof MetaItemContract) {
67 23
                $item->tag = $item->tag ?: $this->default_tag;
1 ignored issue
show
Bug introduced by
Accessing tag on the interface BoxedCode\Eloquent\Meta\Contracts\MetaItem suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
68 23
            }
69
70 23
            return $item;
71 29
        }, $items);
72 29
    }
73
74
    /**
75
     * Get the array of primary keys.
76
     *
77
     * @return array
78
     */
79 29
    public function modelKeys()
80
    {
81 29
        $keys = [];
82
83 29
        foreach ($this->items as $item) {
84 23
            if ($item instanceof MetaItemContract) {
85 23
                $keys[] = $item->getKey();
86 23
            }
87 29
        }
88
89 29
        return $keys;
90
    }
91
92
    /**
93
     * Get the array of primary keys the collection was constructed with.
94
     *
95
     * @return array
96
     */
97 8
    public function originalModelKeys()
98
    {
99 8
        return $this->original_model_keys;
100
    }
101
102
    /**
103
     * Add an item to the collection.
104
     *
105
     * @param mixed $item
106
     * @return $this
107
     * @throws InvalidArgumentException
108
     */
109 11
    public function add($item)
110
    {
111 11
        if ($item instanceof MetaItemContract) {
112 10
            if (! is_null($this->findItem($item->key, $item->tag))) {
113 1
                $tag = $item->tag ?: $this->default_tag;
114
115 1
                $key = $item->key;
116
117 1
                throw new InvalidArgumentException("Unique key / tag constraint failed. [$key/$tag]");
118
            }
119
120 10
            $this->observeDeletions([$item]);
121 10
        }
122
123 11
        $this->items[] = $item;
124
125 11
        return $this;
126
    }
127
128
    /**
129
     * Get the collection key form an item key and tag.
130
     *
131
     * @param mixed $key
132
     * @param null $tag
133
     * @return mixed
134
     */
135 20
    public function findItem($key, $tag = null)
136
    {
137 20
        $collection = $this->whereKey($key);
1 ignored issue
show
Documentation Bug introduced by
The method whereKey does not exist on object<BoxedCode\Eloquen...eta\MetaItemCollection>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
138
139 20
        if (! is_null($tag)) {
140 15
            $collection = $collection->whereTag($tag);
141 15
        }
142
143 20
        if ($collection->count() > 0) {
144 13
            return $collection->keys()->first();
145
        }
146 15
    }
147
148
    /**
149
     * Set deletion listeners on an array of items.
150
     *
151
     * @param array $items
152
     */
153 29
    protected function observeDeletions(array $items)
154
    {
155 29
        foreach ($items as $item) {
156 25
            if ($item instanceof MetaItemContract) {
157 25
                $this->observeDeletion($item);
158 25
            }
159 29
        }
160 29
    }
161
162
    /**
163
     * Set a deletion listener on an item.
164
     *
165
     * @param \BoxedCode\Eloquent\Meta\Contracts\MetaItem $item
166
     */
167
    protected function observeDeletion(MetaItemContract $item)
168
    {
169 25
        $item::deleted(function ($model) {
170 3
            $key = $this->findItem($model->key, $model->tag);
171
172 3
            if (! is_null($key)) {
173 3
                $this->forget($key);
174 3
            }
175 25
        });
176 25
    }
177
178
    /**
179
     * Get the class name that will be used to construct new
180
     * items via the magic methods.
181
     *
182
     * @return string
183
     */
184 1
    public static function getMetaItemClass()
185
    {
186 1
        return static::$item_class;
187
    }
188
189
    /**
190
     * Set the class name that will be used to construct new
191
     * items via the magic methods.
192
     *
193
     * @param $class
194
     */
195 10
    public static function setMetaItemClass($class)
196
    {
197 10
        if (is_object($class)) {
198 9
            $class = get_class($class);
199 9
        }
200
201 10
        static::$item_class = $class;
202 10
    }
203
204
    /**
205
     * Get the default tag name that will be used to construct new
206
     * items via the magic methods.
207
     *
208
     * @return string
209
     */
210 1
    public function getDefaultTag()
211
    {
212 1
        return $this->default_tag;
213
    }
214
215
    /**
216
     * Set the default tag name that will be used to construct new
217
     * items via the magic methods.
218
     *
219
     * @param $name
220
     * @return $this
221
     */
222 2
    public function setDefaultTag($name)
223
    {
224 2
        $this->default_tag = $name;
225
226 2
        return $this;
227
    }
228
229
    /**
230
     * Resolve calls to filter the collection by item attributes.
231
     *
232
     * @param string $name
233
     * @param array $arguments
234
     * @return static
235
     */
236 22
    public function __call($name, $arguments)
237
    {
238 22
        if (starts_with($name, 'where') && 1 === count($arguments)) {
239 22
            $key = snake_case(substr($name, 5));
240
241 22
            return $this->where($key, $arguments[0]);
242
        }
243
    }
244
245
    /**
246
     * Resolve calls to check whether an item with a specific key name exists.
247
     *
248
     * @param $name
249
     * @return bool
250
     */
251 3
    public function __isset($name)
252
    {
253 3
        return ! is_null($this->findItem($name, $this->default_tag));
254
    }
255
256
    /**
257
     * Resolve calls to unset an item with a specific key name.
258
     *
259
     * @param $name
260
     */
261 2
    public function __unset($name)
262
    {
263 2
        $key = $this->findItem($name, $this->default_tag);
264
265 2
        if (! is_null($key)) {
266 1
            $this->forget($key);
267 1
        }
268 2
    }
269
270
    /**
271
     * Resolve calls to get an item with a specific key name or a
272
     * collection of items with a specific tag name.
273
     *
274
     * @param $name
275
     * @return mixed
276
     */
277 7
    public function __get($name)
278
    {
279 7
        $key = $this->findItem($name, $this->default_tag);
280
281 7
        if (! is_null($key)) {
282 5
            return $this->get($key)->value;
283
        }
284
285 3
        $tag = $this->where('tag', $name);
286
287 3
        if ($tag->count() > 0) {
288 1
            return $tag->setDefaultTag($name);
289
        }
290 2
    }
291
292
    /**
293
     * Resolve calls to set a new item to the collection or
294
     * update an existing key.
295
     *
296
     * @param $name
297
     * @param $value
298
     */
299 3
    public function __set($name, $value)
300
    {
301 3
        $key = $this->findItem($name, $this->default_tag);
302
303 3
        if (! is_null($key)) {
304 2
            $this->get($key)->value = $value;
305 2
        } else {
306
            $attr = [
307 1
                'key'   => $name,
308 1
                'value' => $value,
309 1
                'tag'   => $this->default_tag,
310 1
            ];
311
312 1
            $class = static::$item_class;
313
314 1
            $this->add(new $class($attr));
315
        }
316 3
    }
317
}
318