Passed
Push — master ( c7474a...79c34f )
by Chauncey
02:35
created

AttachmentAwareTrait::parseAttachmentOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 1
eloc 6
nc 1
nop 1
1
<?php
2
3
namespace Charcoal\Attachment\Traits;
4
5
use Charcoal\Attachment\Interfaces\AttachmentAwareInterface;
6
use InvalidArgumentException;
7
8
// From 'charcoal-core'
9
use Charcoal\Model\ModelInterface;
10
11
// From 'charcoal-admin'
12
use Charcoal\Admin\Widget\AttachmentWidget;
13
14
// From 'locomotivemtl/charcoal-attachments'
15
use Charcoal\Attachment\Interfaces\AttachableInterface;
16
use Charcoal\Attachment\Interfaces\AttachmentContainerInterface;
17
18
use Charcoal\Attachment\Object\Join;
19
use Charcoal\Attachment\Object\Attachment;
20
21
/**
22
 * Provides support for attachments to objects.
23
 *
24
 * Used by objects that can have an attachment to other objects.
25
 * This is the glue between the {@see Join} object and the current object.
26
 *
27
 * Abstract method needs to be implemented.
28
 *
29
 * Implementation of {@see \Charcoal\Attachment\Interfaces\AttachmentAwareInterface}
30
 *
31
 * ## Required Services
32
 *
33
 * - "model/factory" — {@see \Charcoal\Model\ModelFactory}
34
 * - "model/collection/loader" — {@see \Charcoal\Loader\CollectionLoader}
35
 */
36
trait AttachmentAwareTrait
37
{
38
    /**
39
     * A store of cached attachments, by ID.
40
     *
41
     * @var Attachment[] $attachmentCache
42
     */
43
    protected static $attachmentCache = [];
44
45
    /**
46
     * Store a collection of node objects.
47
     *
48
     * @var Collection|Attachment[]
49
     */
50
    protected $attachments = [];
51
52
    /**
53
     * Store the widget instance currently displaying attachments.
54
     *
55
     * @var AttachmentWidget
56
     */
57
    protected $attachmentWidget;
58
59
    /**
60
     * Retrieve the objects associated to the current object.
61
     *
62
     * @param  array|string|null $group  Filter the attachments by a group identifier.
63
     *                                   When an array, filter the attachments by a options list.
64
     * @param  string|null       $type   Filter the attachments by type.
65
     * @param  callable|null     $before Process each attachment before applying data.
66
     * @param  callable|null     $after  Process each attachment after applying data.
67
     * @throws InvalidArgumentException If the $group or $type is invalid.
68
     * @return Collection|Attachment[]
69
     */
70
    public function attachments(
71
        $group = null,
72
        $type = null,
73
        callable $before = null,
74
        callable $after = null
75
    ) {
76
        if (is_array($group)) {
77
            $options = $group;
78
        } else {
79
            if ($group !== null) {
80
                $this->logger->warning(
0 ignored issues
show
Bug introduced by
The property logger does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
81
                    'AttachmentAwareTrait::attachments() parameters are deprecated. '.
82
                    'An array of parameters should be used.',
83
                    [ 'package' => 'locomotivemtl/charcoal-attachment' ]
84
                );
85
            }
86
            $options = [
87
                'group'  => $group,
88
                'type'   => $type,
89
                'before' => $before,
90
                'after'  => $after,
91
            ];
92
        }
93
94
        $options = $this->parseAttachmentOptions($options);
95
        extract($options);
96
97 View Code Duplication
        if ($group !== 0) {
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...
98
            if (!is_string($group)) {
99
                throw new InvalidArgumentException(sprintf(
100
                    'The "group" must be a string, received %s',
101
                    is_object($group) ? get_class($group) : gettype($group)
102
                ));
103
            }
104
        }
105
106
        if ($type !== 0) {
107 View Code Duplication
            if (!is_string($type)) {
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...
108
                throw new InvalidArgumentException(sprintf(
109
                    'The "type" must be a string, received %s',
110
                    is_object($type) ? get_class($type) : gettype($type)
111
                ));
112
            }
113
114
            $type = preg_replace('/([a-z])([A-Z])/', '$1-$2', $type);
115
            $type = strtolower(str_replace('\\', '/', $type));
116
        }
117
118
        if (isset($this->attachments[$group][$type])) {
119
            return $this->attachments[$group][$type];
120
        }
121
122
        $objType = $this->objType();
123
        $objId   = $this->id();
124
125
        $joinProto = $this->modelFactory()->get(Join::class);
126
        $joinTable = $joinProto->source()->table();
127
128
        $attProto = $this->modelFactory()->get(Attachment::class);
129
        $attTable = $attProto->source()->table();
130
131
        if (!$attProto->source()->tableExists() || !$joinProto->source()->tableExists()) {
132
            return [];
133
        }
134
135
        $widget = $this->attachmentWidget();
136
137
        $query = sprintf('
138
            SELECT
139
                attachment.*,
140
                joined.attachment_id AS attachment_id,
141
                joined.position AS position
142
            FROM
143
                `%s` AS attachment
144
            LEFT JOIN
145
                `%s` AS joined
146
            ON
147
                joined.attachment_id = attachment.id
148
            WHERE
149
                1 = 1', $attTable, $joinTable);
150
151
        /** Disable `active` check in admin, or according to $isActive value */
152
        if (!$widget instanceof AttachmentWidget && $isActive === true) {
153
            $query .= '
154
            AND
155
                attachment.active = 1';
156
        }
157
158
        if ($type) {
159
            $query .= sprintf('
160
            AND
161
                attachment.type = "%s"', $type);
162
        }
163
164
        $query .= sprintf('
165
            AND
166
                joined.object_type = "%s"
167
            AND
168
                joined.object_id = "%s"', $objType, $objId);
169
170
        if ($group) {
171
            $query .= sprintf('
172
            AND
173
                joined.group = "%s"', $group);
174
        }
175
176
        $query .= '
177
            ORDER BY joined.position';
178
179
        $loader = $this->collectionLoader();
180
        $loader->setModel($attProto);
181
        $loader->setDynamicTypeField('type');
182
183
        if ($widget instanceof AttachmentWidget) {
184
            $callable = function (&$att) use ($widget, $before) {
185
                if ($this instanceof AttachableInterface) {
186
                    $att->setContainerObj($this);
187
                }
188
189
                if ($att instanceof AttachmentAwareInterface) {
190
                    $att['attachment_widget'] = $widget;
191
                }
192
193
                $kind = $att->type();
194
                $attachables = $widget->attachableObjects();
195
196
                if (isset($attachables[$kind]['data'])) {
197
                    $att->setData($attachables[$kind]['data']);
198
                }
199
200
                if (!$att->rawHeading()) {
201
                    $att->setHeading($widget->attachmentHeading());
202
                }
203
204
                if (!$att->rawPreview()) {
205
                    $att->setPreview($widget->attachmentPreview());
206
                }
207
208
                $att->isPresentable(true);
209
210
                if ($before !== null) {
211
                    call_user_func_array($before, [ &$att ]);
212
                }
213
            };
214
        } else {
215
            $callable = function (&$att) use ($before) {
216
                if ($this instanceof AttachableInterface) {
217
                    $att->setContainerObj($this);
218
                }
219
220
                $att->isPresentable(true);
221
222
                if ($before !== null) {
223
                    call_user_func_array($before, [ &$att ]);
224
                }
225
            };
226
        }
227
228
        $collection = $loader->loadFromQuery($query, $after, $callable->bindTo($this));
229
230
        $this->attachments[$group][$type] = $collection;
231
232
        return $this->attachments[$group][$type];
233
    }
234
235
    /**
236
     * Determine if the current object has any nodes.
237
     *
238
     * @return boolean Whether $this has any nodes (TRUE) or not (FALSE).
239
     */
240
    public function hasAttachments()
241
    {
242
        return !!($this->numAttachments());
243
    }
244
245
    /**
246
     * Count the number of nodes associated to the current object.
247
     *
248
     * @return integer
249
     */
250
    public function numAttachments()
251
    {
252
        return count($this->attachments([
253
            'group' => null
254
        ]));
255
    }
256
257
    /**
258
     * Attach an node to the current object.
259
     *
260
     * @param  AttachableInterface|ModelInterface $attachment An attachment or object.
261
     * @param  string                             $group      Attachment group, defaults to contents.
262
     * @return boolean|self
263
     */
264
    public function addAttachment($attachment, $group = 'contents')
265
    {
266
        if (!$attachment instanceof AttachableInterface && !$attachment instanceof ModelInterface) {
267
            return false;
268
        }
269
270
        $join = $this->modelFactory()->create(Join::class);
271
272
        $objId   = $this->id();
273
        $objType = $this->objType();
274
        $attId   = $attachment->id();
275
276
        $join->setAttachmentId($attId);
277
        $join->setObjectId($objId);
278
        $join->setGroup($group);
279
        $join->setObjectType($objType);
280
281
        $join->save();
282
283
        return $this;
284
    }
285
286
    /**
287
     * Remove all joins linked to a specific attachment.
288
     *
289
     * @deprecated in favour of AttachmentAwareTrait::removeAttachmentJoins()
290
     * @return boolean
291
     */
292
    public function removeJoins()
293
    {
294
        $this->logger->warning(
295
            'AttachmentAwareTrait::removeJoins() is deprecated. '.
296
            'Use AttachmentAwareTrait::removeAttachmentJoins() instead.',
297
            [ 'package' => 'locomotivemtl/charcoal-attachment' ]
298
        );
299
300
        return $this->removeAttachmentJoins();
301
    }
302
303
    /**
304
     * Remove all joins linked to a specific attachment.
305
     *
306
     * @return boolean
307
     */
308
    public function removeAttachmentJoins()
309
    {
310
        $joinProto = $this->modelFactory()->get(Join::class);
311
312
        $loader = $this->collectionLoader();
313
        $loader
314
            ->setModel($joinProto)
315
            ->addFilter('object_type', $this->objType())
316
            ->addFilter('object_id', $this->id());
317
318
        $collection = $loader->load();
319
320
        foreach ($collection as $obj) {
321
            $obj->delete();
322
        }
323
324
        return true;
325
    }
326
327
    /**
328
     * Retrieve the attachment widget.
329
     *
330
     * @return AttachmentWidget
331
     */
332
    protected function attachmentWidget()
333
    {
334
        return $this->attachmentWidget;
335
    }
336
337
    /**
338
     * Set the attachment widget.
339
     *
340
     * @param  AttachmentWidget $widget The widget displaying attachments.
341
     * @return string
342
     */
343
    protected function setAttachmentWidget(AttachmentWidget $widget)
344
    {
345
        $this->attachmentWidget = $widget;
346
347
        return $this;
348
    }
349
350
    /**
351
     * Available attachment obj_type related to the current object.
352
     * This goes throught the entire forms / form groups, starting from the
353
     * dashboard widgets.
354
     * Returns an array of object classes by group
355
     * [
356
     *    group : [
357
     *        'object\type',
358
     *        'object\type2',
359
     *        'object\type3'
360
     *    ]
361
     * ]
362
     * @return array Attachment obj_types.
363
     */
364
    public function attachmentObjTypes()
365
    {
366
        $defaultEditDashboard = $this->metadata()->get('admin.default_edit_dashboard');
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
367
        $dashboards = $this->metadata()->get('admin.dashboards');
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
368
        $editDashboard = $dashboards[$defaultEditDashboard];
369
        $widgets = $editDashboard['widgets'];
370
371
        $formIdent = '';
372
        foreach ($widgets as $ident => $val) {
373
            if ($val['type'] == 'charcoal/admin/widget/objectForm') {
374
                $formIdent = $val['form_ident'];
375
            }
376
        }
377
378
        if (!$formIdent) {
379
            // No good!
380
            return [];
381
        }
382
383
        // Current form
384
        $form = $this->metadata()->get('admin.forms.'.$formIdent);
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
385
386
        // Setted form gruops
387
        $formGroups = $this->metadata()->get('admin.form_groups');
0 ignored issues
show
Bug introduced by
It seems like metadata() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
388
389
        // Current form groups
390
        $groups = $form['groups'];
391
392
        $attachmentObjects = [];
393
        foreach ($groups as $groupIdent => $group) {
394
            if (isset($formGroups[$groupIdent])) {
395
                $group = array_replace_recursive(
396
                    $formGroups[$groupIdent],
397
                    $group
398
                );
399
            }
400
401
            if (isset($group['attachable_objects'])) {
402
                $attachmentObjects[$group['group']] = [];
403
                foreach ($group['attachable_objects'] as $type => $content) {
404
                    $attachmentObjects[$group['group']][] = $type;
405
                }
406
            }
407
        }
408
409
        return $attachmentObjects;
410
    }
411
412
    /**
413
     * Parse a given options for loading a collection of attachments.
414
     *
415
     * @param  array $options A list of options.
416
     *    Option keys not present in {@see self::getDefaultAttachmentOptions() default options}
417
     *    are rejected.
418
     * @return array
419
     */
420
    protected function parseAttachmentOptions(array $options)
421
    {
422
        $defaults = $this->getDefaultAttachmentOptions();
423
424
        $options = array_intersect_key($options, $defaults);
425
        $options = array_filter($options, [ $this, 'filterAttachmentOption' ], ARRAY_FILTER_USE_BOTH);
426
        $options = array_replace($defaults, $options);
427
428
        return $options;
429
    }
430
431
    /**
432
     * Parse a given options for loading a collection of attachments.
433
     *
434
     * @param  mixed  $val The option value.
435
     * @param  string $key The option key.
436
     * @return boolean Return TRUE if the value is preserved. Otherwise FALSE.
437
     */
438
    protected function filterAttachmentOption($val, $key)
439
    {
440
        if ($val === null) {
441
            return false;
442
        }
443
444
        switch ($key) {
445
            case 'isActive':
446
                return is_bool($val);
447
448
            case 'before':
449
            case 'after':
450
                return is_callable($val);
451
        }
452
453
        return true;
454
    }
455
456
    /**
457
     * Retrieve the default options for loading a collection of attachments.
458
     *
459
     * @return array
460
     */
461
    protected function getDefaultAttachmentOptions()
462
    {
463
        return [
464
            'group'    => 0,
465
            'type'     => 0,
466
            'before'   => null,
467
            'after'    => null,
468
            'isActive' => true
469
        ];
470
    }
471
472
473
474
    // Abstract Methods
475
    // =========================================================================
476
477
    /**
478
     * Retrieve the object's type identifier.
479
     *
480
     * @return string
481
     */
482
    abstract public function objType();
483
484
    /**
485
     * Retrieve the object's unique ID.
486
     *
487
     * @return mixed
488
     */
489
    abstract public function id();
490
491
    /**
492
     * Retrieve the object model factory.
493
     *
494
     * @return \Charcoal\Factory\FactoryInterface
495
     */
496
    abstract public function modelFactory();
497
498
    /**
499
     * Retrieve the model collection loader.
500
     *
501
     * @return \Charcoal\Loader\CollectionLoader
502
     */
503
    abstract public function collectionLoader();
504
}
505