Issues (134)

src/api/mgdobject.php (16 issues)

1
<?php
2
/**
3
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
4
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
6
 */
7
namespace midgard\portable\api;
8
9
use midgard\portable\storage\connection;
10
use midgard\portable\storage\objectmanager;
11
use midgard\portable\storage\collection;
12
use midgard\portable\storage\interfaces\metadata as metadata_interface;
13
use midgard\portable\mgdschema\translator;
14
use midgard\portable\api\error\exception;
15
use Doctrine\ORM\Query;
16
use midgard_connection;
17
use Doctrine\ORM\Proxy\Proxy;
18
use Doctrine\ORM\QueryBuilder;
19
use Doctrine\ORM\EntityNotFoundException;
20
21
/**
22
 * @property metadata $metadata
23
 */
24
abstract class mgdobject extends dbobject
25
{
26
    protected $metadata; // compat with mgd behavior: If the schema has no metadata, the property is present anyway
27
28
    public $action = ''; // <== does this need to do anything?
29
30
    private array $collections = [];
31
32
    /**
33
     * @param mixed $id ID or GUID
34
     */
35 137
    public function __construct($id = null)
36
    {
37 137
        if ($id !== null) {
38 50
            if (is_int($id)) {
39 41
                $this->get_by_id($id);
40 14
            } elseif (is_string($id)) {
41 14
                $this->get_by_guid($id);
42
            }
43
        }
44
    }
45
46 16
    private function get_collection(string $classname) : collection
47
    {
48 16
        if (!isset($this->collections[$classname])) {
49 16
            $this->collections[$classname] = new collection($classname);
50
        }
51 16
        return $this->collections[$classname];
52
    }
53
54 1
    public function __debugInfo()
55
    {
56 1
        $ret = parent::__debugInfo();
57 1
        if (property_exists($this, 'metadata')) {
58 1
            $metadata = new \stdClass;
59 1
            foreach ($this->cm->getFieldNames() as $name) {
60 1
                if (str_contains($name, 'metadata_')) {
61 1
                    $fieldname = str_replace('metadata_', '', $name);
62 1
                    $metadata->$fieldname = $this->__get($name);
63
                }
64
            }
65 1
            $ret['metadata'] = $metadata;
66
        }
67
68 1
        return $ret;
69
    }
70
71 113
    public function __set($field, $value)
72
    {
73 113
        if ($field == 'guid') {
74 6
            return;
75
        }
76 113
        parent::__set($field, $value);
77
    }
78
79 130
    public function __get($field)
80
    {
81 130
        if (   $field === 'metadata'
82 130
            && $this->metadata === null
83 130
            && $this instanceof metadata_interface) {
84 104
            $this->metadata = new metadata($this);
85
        }
86
87 130
        return parent::__get($field);
88
    }
89
90 1
    public function __call($method, $args)
91
    {
92 1
        if ($method === 'list') {
93 1
            return $this->_list();
94
        }
95
        throw new \BadMethodCallException("Unknown method " . $method . " on " . get_class($this));
96
    }
97
98 4
    protected function load_parent(array $candidates) : ?dbobject
99
    {
100 4
        foreach ($candidates as $candidate) {
101 4
            if (   is_string($this->$candidate)
102 4
                && mgd_is_guid($this->$candidate)) {
103
                return \midgard_object_class::get_object_by_guid($this->$candidate);
104
            }
105 4
            if ($this->$candidate !== null) {
106
                //Proxies become stale if the object itself is detached, so we have to re-fetch
107 4
                if (   $this->$candidate instanceof Proxy
108 4
                    && $this->$candidate->__isInitialized()) {
109
                    try {
110 1
                        $this->$candidate->get_by_id($this->$candidate->id);
111
                    } catch (exception $e) {
112
                        connection::log()->error('Failed to refresh parent from proxy: ' . $e->getMessage());
113
                        return null;
114
                    }
115
                }
116 4
                return $this->$candidate;
117
            }
118
        }
119 1
        return null;
120
    }
121
122 43
    public function get_by_id(int $id) : bool
123
    {
124 43
        $entity = connection::get_em()->find(get_class($this), $id);
125
126 43
        if ($entity === null) {
127 3
            throw exception::not_exists();
128
        }
129
        // Normally, the proxy should be automatically loaded when accessing e.g. $entity->guid,
130
        // because that triggers the proxy's magic __get method, which initializes. But since mgdobject
131
        // is the entity's (grand)parent class, PHP rules dictate that we have access to
132
        // its parents' protected variables. Hence, we need to load explicitly.
133 42
        if ($entity instanceof Proxy && !$entity->__isInitialized()) {
134
            try {
135 7
                $entity->__load();
136 1
            } catch (EntityNotFoundException) {
137 1
                throw exception::object_purged();
138
            }
139
        }
140 42
        if ($entity instanceof metadata_interface && $entity->{metadata_interface::DELETED_FIELD}) {
141
            // This can happen when the "deleted" entity is still in EM's identity map
142
            throw exception::object_deleted();
143
        }
144
145 42
        $this->populate_from_entity($entity);
146
147 42
        connection::get_em()->detach($entity);
148 42
        exception::ok();
149 42
        return true;
150
    }
151
152 16
    public function get_by_guid(string $guid) : bool
153
    {
154 16
        if (!mgd_is_guid($guid)) {
155 1
            throw new \InvalidArgumentException("'$guid' is not a valid guid");
156
        }
157 15
        $entity = connection::get_em()->getRepository(get_class($this))->findOneBy(['guid' => $guid]);
158 15
        if ($entity === null) {
159
            throw exception::not_exists();
160
        }
161 15
        $this->populate_from_entity($entity);
162
163 15
        connection::get_em()->detach($entity);
164 15
        exception::ok();
165 15
        return true;
166
    }
167
168 110
    public function create() : bool
169
    {
170 110
        $this->initialize();
171
172 110
        if ($this->cm->getIdentifierValues($this)) {
173 2
            exception::duplicate();
174 2
            return false;
175
        }
176 110
        if (   !$this->is_unique()
177 110
            || !$this->check_parent()) {
178 2
            return false;
179
        }
180 110
        if (!$this->check_fields()) {
181 1
            return false;
182
        }
183
        try {
184 109
            $om = new objectmanager(connection::get_em());
185 109
            $om->create($this);
186 1
        } catch (\Exception $e) {
187 1
            exception::internal($e);
188 1
            return false;
189
        }
190 109
        return (bool) $this->cm->getIdentifierValues($this);
191
    }
192
193 16
    public function update() : bool
194
    {
195 16
        $this->initialize();
196
197 16
        if (!$this->cm->getIdentifierValues($this)) {
198 1
            midgard_connection::get_instance()->set_error(MGD_ERR_INTERNAL);
199 1
            return false;
200
        }
201 15
        if (!$this->check_fields()) {
202 2
            return false;
203
        }
204
        try {
205 13
            $om = new objectmanager(connection::get_em());
206 13
            $om->update($this);
207
        } catch (\Exception $e) {
208
            exception::internal($e);
209
            return false;
210
        }
211
212 13
        return true;
213
    }
214
215
    /**
216
     * @todo: Tests indicate that $check_dependencies is ignored in the mgd2 extension,
217
     * so we might consider ignoring it, too
218
     */
219 29
    public function delete(bool $check_dependencies = true) : bool
220
    {
221 29
        $this->initialize();
222
223 29
        if (!$this->cm->getIdentifierValues($this)) {
224 1
            exception::invalid_property_value();
225 1
            return false;
226
        }
227 28
        if (   $check_dependencies
228 28
            && $this->has_dependents()) {
229 4
            exception::has_dependants();
230 4
            return false;
231
        }
232 28
        if (!($this instanceof metadata_interface)) {
233 1
            exception::invalid_property_value();
234 1
            return false;
235
        }
236 27
        if ($this->{metadata_interface::DELETED_FIELD}) {
237 1
            return true;
238
        }
239
240
        try {
241 27
            $om = new objectmanager(connection::get_em());
242 27
            $om->delete($this);
243
        } catch (\Exception $e) {
244
            exception::internal($e);
245
            return false;
246
        }
247
248 27
        return true;
249
    }
250
251 110
    private function is_unique() : bool
252
    {
253 110
        if (empty($this->cm->midgard['unique_fields'])) {
254 104
            return true;
255
        }
256
257 8
        $qb = connection::get_em()->createQueryBuilder();
258 8
        $qb->from(get_class($this), 'c');
259 8
        $conditions = $qb->expr()->andX();
260 8
        $parameters = [];
261 8
        if ($identifier = $this->cm->getIdentifierValues($this)) {
262
            $parameters = array_merge($parameters, $identifier);
263
            $field = key($identifier);
264
            $conditions->add($qb->expr()->neq('c.' . $field, ':' . $field));
265
        }
266 8
        $found = false;
267 8
        foreach ($this->cm->midgard['unique_fields'] as $field) {
268 8
            if (empty($this->$field)) {
269
                //empty names automatically pass according to Midgard logic
270 2
                continue;
271
            }
272 7
            $conditions->add($qb->expr()->eq('c.' . $field, ':' . $field));
273 7
            $parameters[$field] = $this->$field;
274 7
            $found = true;
275
        }
276
277 8
        if (!$found) {
278 2
            return true;
279
        }
280
281 7
        foreach (['upfield', 'parentfield'] as $candidate) {
282 7
            if (!empty($this->cm->midgard[$candidate])) {
283
                // TODO: This needs to be changed so that value is always numeric, since this is how midgard does it
284 6
                if ($this->{$this->cm->midgard[$candidate]} === null) {
285 6
                    $conditions->add($qb->expr()->isNull('c.' . $this->cm->midgard[$candidate]));
286
                } else {
287 6
                    $conditions->add($qb->expr()->eq('c.' . $this->cm->midgard[$candidate], ':' . $this->cm->midgard[$candidate]));
288 6
                    $parameters[$this->cm->midgard[$candidate]] = $this->{$this->cm->midgard[$candidate]};
289
                }
290 6
                break;
291
            }
292
        }
293
294 7
        $qb->where($conditions)
295 7
            ->setParameters($parameters);
296
297 7
        $qb->select("count(c)");
298 7
        $count = (int) $qb->getQuery()->getSingleScalarResult();
299
300 7
        if ($count !== 0) {
301 1
            exception::object_name_exists();
302 1
            return false;
303
        }
304 7
        return true;
305
    }
306
307 110
    private function check_parent() : bool
308
    {
309 110
        if (   empty($this->cm->midgard['parentfield'])
310 110
            || empty($this->cm->midgard['parent'])) {
311 110
            return true;
312
        }
313
314 8
        if (empty($this->{$this->cm->midgard['parentfield']})) {
315 1
            exception::object_no_parent();
316 1
            return false;
317
        }
318 8
        return true;
319
    }
320
321 110
    private function check_fields() : bool
322
    {
323 110
        foreach ($this->cm->fieldMappings as $name => $field) {
324 110
            if (   $field['midgard:midgard_type'] == translator::TYPE_GUID
325 110
                && !empty($this->$name)
326 110
                && !mgd_is_guid($this->$name)) {
327 2
                exception::invalid_property_value("'" . $name . "' property's value is not a guid.");
328 2
                return false;
329
            }
330
        }
331 109
        return $this->check_upfield();
332
    }
333
334 109
    private function check_upfield() : bool
335
    {
336 109
        $identifier = $this->cm->getIdentifierValues($this);
337 109
        if (   $identifier
0 ignored issues
show
Bug Best Practice introduced by
The expression $identifier of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
338 109
            && !empty($this->cm->midgard['upfield'])
339 109
            && $this->__get($this->cm->midgard['upfield']) === reset($identifier)
340 109
            && $this->cm->getAssociationMapping($this->cm->midgard['upfield'])['targetEntity'] === $this->cm->getName()) {
341 1
            exception::tree_is_circular();
342 1
            return false;
343
        }
344
        // @todo this should be recursive
345 109
        return true;
346
    }
347
348
    public function is_in_parent_tree($root_id, $id) : bool
349
    {
350
        return false;
351
    }
352
353
    public function is_in_tree($root_id, $id) : bool
354
    {
355
        return false;
356
    }
357
358 35
    public function has_dependents() : bool
359
    {
360 35
        $this->initialize();
361
362 35
        $stat = false;
363
364 35
        if (!empty($this->cm->midgard['upfield'])) {
365 30
            $identifier = $this->cm->getIdentifierValues($this);
366 30
            $qb = connection::get_em()->createQueryBuilder();
367 30
            $qb->from(get_class($this), 'c')
368 30
                ->where('c.' . $this->cm->midgard['upfield'] . ' = ?0')
369 30
                ->setParameter(0, (int) reset($identifier))
370 30
                ->select("COUNT(c)");
371 30
            $results = (int) $qb->getQuery()->getSingleScalarResult();
372 30
            $stat = $results > 0;
373
        }
374
375 35
        if (   !$stat
376 35
            && !empty($this->cm->midgard['childtypes'])) {
377 28
            foreach ($this->cm->midgard['childtypes'] as $typename => $parentfield) {
378 28
                $identifier = $this->cm->getIdentifierValues($this);
379 28
                $qb = connection::get_em()->createQueryBuilder();
380 28
                $qb->from(connection::get_fqcn($typename), 'c')
381 28
                    ->where('c.' . $parentfield . ' = ?0')
382 28
                    ->setParameter(0, (int) reset($identifier))
383 28
                    ->select("COUNT(c)");
384
385 28
                $results = (int) $qb->getQuery()->getSingleScalarResult();
386 28
                $stat = $results > 0;
387 28
                if ($stat) {
388 3
                    break;
389
                }
390
            }
391
        }
392
393 35
        return $stat;
394
    }
395
396
    public function get_parent()
397
    {
398
        return null;
399
    }
400
401
    /**
402
     * This function is called list() in Midgard, but that doesn't work in plain PHP
403
     */
404 1
    private function _list() : array
405
    {
406 1
        $this->initialize();
407
408 1
        if (!empty($this->cm->midgard['upfield'])) {
409 1
            $identifier = $this->cm->getIdentifierValues($this);
410 1
            $qb = connection::get_em()->createQueryBuilder();
411 1
            $qb->from(get_class($this), 'c')
412 1
                ->where('c.' . $this->cm->midgard['upfield'] . ' = ?0')
413 1
                ->setParameter(0, (int) reset($identifier))
414 1
                ->select("c");
415 1
            return $qb->getQuery()->getResult();
416
        }
417
418
        return [];
419
    }
420
421
    /**
422
     * This should return child objects, but only if they are of a different type
423
     * For all other input, an empty array is returned
424
     * (not implemented yet)
425
     */
426
    public function list_children(string $classname) : array
427
    {
428
        return [];
429
    }
430
431 1
    public function get_by_path(string $path) : bool
432
    {
433 1
        $parts = explode('/', trim($path, '/'));
434 1
        if (empty($parts)) {
435
            return false;
436
        }
437 1
        $this->initialize();
438
439 1
        if (count($this->cm->midgard['unique_fields']) != 1) {
440
            return false;
441
        }
442
443 1
        $field = $this->cm->midgard['unique_fields'][0];
444
445 1
        if (!empty($this->cm->midgard['parent'])) {
446 1
            $parent_cm = connection::get_em()->getClassMetadata(connection::get_fqcn($this->cm->midgard['parent']));
447 1
            $parentclass = $this->cm->fullyQualifiedClassName($this->cm->midgard['parent']);
448 1
            $parentfield = $parent_cm->midgard['upfield'];
0 ignored issues
show
Accessing midgard on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
449 1
            $upfield = $this->cm->midgard['parentfield'];
450 1
        } elseif (!empty($this->cm->midgard['upfield'])) {
451 1
            $parentclass = get_class($this);
452 1
            $upfield = $this->cm->midgard['upfield'];
453 1
            $parentfield = $upfield;
454
        } else {
455
            return false;
456
        }
457
458 1
        $name = array_pop($parts);
459 1
        $up = 0;
460 1
        foreach ($parts as $part) {
461 1
            $qb = $this->get_uniquefield_query($parentclass, $field, $part, $parentfield, $up);
0 ignored issues
show
It seems like $parentclass can also be of type null; however, parameter $classname of midgard\portable\api\mgd...get_uniquefield_query() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

461
            $qb = $this->get_uniquefield_query(/** @scrutinizer ignore-type */ $parentclass, $field, $part, $parentfield, $up);
Loading history...
462 1
            $qb->select("c.id");
463 1
            $up = (int) $qb->getQuery()->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR);
464 1
            if ($up === 0) {
465 1
                return $this->reset();
466
            }
467
        }
468
469 1
        $qb = $this->get_uniquefield_query(get_class($this), $field, $name, $upfield, $up);
470 1
        $qb->select("c");
471
472 1
        $entity = $qb->getQuery()->getOneOrNullResult();
473
474 1
        if ($entity === null) {
475 1
            return $this->reset();
476
        }
477 1
        $this->populate_from_entity($entity);
0 ignored issues
show
It seems like $entity can also be of type integer; however, parameter $entity of midgard\portable\api\dbo...:populate_from_entity() does only seem to accept midgard\portable\api\dbobject, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

477
        $this->populate_from_entity(/** @scrutinizer ignore-type */ $entity);
Loading history...
478
479 1
        return true;
480
    }
481
482 1
    private function reset() : bool
483
    {
484 1
        exception::not_exists();
485 1
        foreach ($this->cm->getIdentifierFieldNames() as $field) {
486 1
            $this->$field = 0;
487
        }
488 1
        $this->set_guid('');
489 1
        return false;
490
    }
491
492 1
    protected function get_uniquefield_query(string $classname, string $field, string $part, string $upfield, int $up) : QueryBuilder
493
    {
494 1
        $qb = connection::get_em()->createQueryBuilder();
495 1
        $qb->from($classname, 'c');
496 1
        $conditions = $qb->expr()->andX();
497 1
        $conditions->add($qb->expr()->eq('c.' . $field, ':' . $field));
498 1
        $parameters = [
499 1
            $field => $part
500 1
        ];
501
502 1
        if (empty($up)) {
503
            // If the database was created by Midgard, it might contain 0 instead of NULL, so...
504 1
            $empty_conditions = $qb->expr()->orX()
505 1
                ->add($qb->expr()->isNull('c.' . $upfield))
506 1
                ->add($qb->expr()->eq('c.' . $upfield, '0'));
507 1
            $conditions->add($empty_conditions);
508
        } else {
509 1
            $conditions->add($qb->expr()->eq('c.' . $upfield, ':' . $upfield));
510 1
            $parameters[$upfield] = $up;
511
        }
512
513 1
        $qb->where($conditions)
514 1
            ->setParameters($parameters);
515
516 1
        return $qb;
517
    }
518
519
    /**
520
     * @return boolean
521
     */
522
    public function parent()
523
    {
524
        return false;
525
    }
526
527 1
    public function has_parameters() : bool
528
    {
529 1
        return !$this->get_collection('midgard_parameter')->is_empty($this->guid);
530
    }
531
532 4
    public function list_parameters(?string $domain = null) : array
533
    {
534 4
        $constraints = [];
535 4
        if ($domain) {
536 1
            $constraints = ["domain" => $domain];
537
        }
538
539 4
        return $this->get_collection('midgard_parameter')->find($this->guid, $constraints);
540
    }
541
542 3
    public function find_parameters(array $constraints = []) : array
543
    {
544 3
        return $this->get_collection('midgard_parameter')->find($this->guid, $constraints);
545
    }
546
547 1
    public function delete_parameters(array $constraints = []) : int
548
    {
549 1
        return $this->get_collection('midgard_parameter')->delete($this->guid, $constraints);
550
    }
551
552 1
    public function purge_parameters(array $constraints = []) : int
553
    {
554 1
        return $this->get_collection('midgard_parameter')->purge($this->guid, $constraints);
555
    }
556
557 2
    public function get_parameter(string $domain, string $name)
558
    {
559 2
        if (!$this->guid) {
560 1
            return false;
561
        }
562 2
        $qb = connection::get_em()->createQueryBuilder();
563 2
        $qb
564 2
            ->select('c.value')
565 2
            ->from(connection::get_fqcn('midgard_parameter'), 'c')
566 2
            ->where('c.domain = :domain AND c.name = :name AND c.parentguid = :parentguid')
567 2
            ->setParameters(['domain' => $domain, 'name' => $name, 'parentguid' => $this->guid]);
568
569 2
        return $qb->getQuery()->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR);
570
    }
571
572 11
    public function set_parameter(string $domain, string $name, $value) : bool
573
    {
574 11
        $constraints = [
575 11
            'domain' => $domain,
576 11
            'name' => $name,
577 11
        ];
578 11
        $params = $this->get_collection('midgard_parameter')->find($this->guid, $constraints);
579
580
        // check value
581 11
        if (in_array($value, [false, null, ''], true)) {
582 2
            if (empty($params)) {
583 1
                exception::not_exists();
584 1
                return false;
585
            }
586 2
            foreach ($params as $param) {
587 2
                $stat = $param->delete();
588
            }
589 2
            return $stat;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $stat seems to be defined by a foreach iteration on line 586. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
590
        }
591
592 11
        $om = new objectmanager(connection::get_em());
593
        try {
594
            // create new
595 11
            if (empty($params)) {
596 11
                $parameter = $om->new_instance(connection::get_fqcn('midgard_parameter'));
597 11
                $parameter->parentguid = $this->guid;
0 ignored issues
show
Bug Best Practice introduced by
The property parentguid does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
598 11
                $parameter->domain = $domain;
0 ignored issues
show
Bug Best Practice introduced by
The property domain does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
599 11
                $parameter->name = $name;
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
600 11
                $parameter->value = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property value does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
601 11
                $om->create($parameter);
602
            }
603
            // use existing
604
            else {
605 1
                $parameter = $params[0];
606 1
                $parameter->value = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property value does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
607 1
                $om->update($parameter);
608
            }
609 11
            return true;
610
        } catch (\Exception $e) {
611
            exception::internal($e);
612
            return false;
613
        }
614
    }
615
616
    /**
617
     * The signature is a little different from original, because Doctrine doesn't support func_get_args() in proxies
618
     */
619 2
    public function parameter(string $domain, string $name, $value = '__UNINITIALIZED__')
620
    {
621 2
        if ($value === '__UNINITIALIZED__') {
622 1
            return $this->get_parameter($domain, $name);
623
        }
624 2
        return $this->set_parameter($domain, $name, $value);
625
    }
626
627 1
    public function has_attachments() : bool
628
    {
629 1
        return !$this->get_collection('midgard_attachment')->is_empty($this->guid);
630
    }
631
632 2
    public function list_attachments() : array
633
    {
634 2
        return $this->get_collection('midgard_attachment')->find($this->guid, []);
635
    }
636
637
    public function find_attachments(array $constraints = []) : array
638
    {
639
        return $this->get_collection('midgard_attachment')->find($this->guid, $constraints);
640
    }
641
642
    public function delete_attachments(array $constraints = []) : int
643
    {
644
        return $this->get_collection('midgard_attachment')->delete($this->guid, $constraints);
645
    }
646
647
    /**
648
     * @return boolean False if one or more attachments couldn't be deleted
649
     * @todo Implement delete_blob & return value
650
     */
651
    public function purge_attachments(array $constraints = [], bool $delete_blob = true)
652
    {
653
        return $this->get_collection('midgard_attachment')->purge($this->guid, $constraints);
654
    }
655
656 3
    public function create_attachment(string $name, string $title = '', string $mimetype = '') : ?attachment
657
    {
658 3
        $existing = $this->get_collection('midgard_attachment')->find($this->guid, ['name' => $name]);
659 3
        if (!empty($existing)) {
660 1
            exception::object_name_exists();
661 1
            return null;
662
        }
663 3
        $om = new objectmanager(connection::get_em());
664 3
        $att = $om->new_instance(connection::get_fqcn('midgard_attachment'));
665
666 3
        $att->parentguid = $this->guid;
0 ignored issues
show
Bug Best Practice introduced by
The property parentguid does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
667 3
        $att->title = $title;
0 ignored issues
show
Bug Best Practice introduced by
The property title does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
668 3
        $att->name = $name;
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
669 3
        $att->mimetype = $mimetype;
0 ignored issues
show
Bug Best Practice introduced by
The property mimetype does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
670
        try {
671 3
            $om->create($att);
672 3
            return $att;
673
        } catch (\Exception $e) {
674
            exception::internal($e);
675
            return null;
676
        }
677
    }
678
679
    /**
680
     * @todo: Tests indicate that $check_dependencies is ignored in the mgd2 extension,
681
     * so we might consider ignoring it, too
682
     */
683 17
    public function purge(bool $check_dependencies = true) : bool
684
    {
685 17
        if (!$this->cm->getIdentifierValues($this)) {
686
            // This usually means that the object has been purged already
687
            exception::not_exists();
688
            return false;
689
        }
690 17
        if (   $check_dependencies
691 17
            && $this->has_dependents()) {
692 2
            exception::has_dependants();
693 2
            return false;
694
        }
695
696
        try {
697 17
            $om = new objectmanager(connection::get_em());
698 17
            $om->purge($this);
699 2
        } catch (\Doctrine\ORM\EntityNotFoundException) {
700 2
            exception::not_exists();
701 2
            return false;
702
        } catch (\Exception $e) {
703
            exception::internal($e);
704
            return false;
705
        }
706
707 17
        return true;
708
    }
709
710 2
    public static function undelete(string $guid) : bool
711
    {
712 2
        return \midgard_object_class::undelete($guid);
713
    }
714
715 1
    public static function new_query_builder() : \midgard_query_builder
716
    {
717 1
        return new \midgard_query_builder(static::class);
718
    }
719
720 1
    public static function new_collector(string $field, $value) : \midgard_collector
721
    {
722 1
        return new \midgard_collector(static::class, $field, $value);
723
    }
724
725
    public static function new_reflection_property() : \midgard_reflection_property
726
    {
727
        return new \midgard_reflection_property(static::class);
728
    }
729
730 110
    public function set_guid(string $guid)
731
    {
732 110
        parent::__set('guid', $guid);
733
    }
734
735
    /**
736
     * Helper for managing the isapproved and islocked metadata properties
737
     */
738 8
    private function manage_meta_property(string $action, bool $value) : bool
739
    {
740 8
        if (!($this instanceof metadata_interface)) {
741 4
            exception::no_metadata();
742 4
            return false;
743
        }
744 4
        $user = connection::get_user();
745 4
        if ($user === null) {
746 4
            exception::access_denied();
747 4
            return false;
748
        }
749 4
        if ($action == 'lock') {
750 2
            $flag = 'islocked';
751 2
        } elseif ($action == 'approve') {
752 2
            $flag = 'isapproved';
753
        } else {
754
            throw new exception('Unsupported action ' . $action);
755
        }
756
        // same val
757 4
        if ($this->__get('metadata')->$flag === $value) {
758 3
            return false;
759
        }
760 4
        if ($value === false) {
761 2
            $action = 'un' . $action;
762
        }
763
764 4
        if ($this->cm->getIdentifierValues($this)) {
765
            try {
766 4
                $om = new objectmanager(connection::get_em());
767 4
                $om->{$action}($this);
768
            } catch (\Exception $e) {
769
                exception::internal($e);
770
                return false;
771
            }
772
        }
773
774 4
        return true;
775
    }
776
777 3
    public function approve() : bool
778
    {
779 3
        return $this->manage_meta_property("approve", true);
780
    }
781
782 2
    public function is_approved() : bool
783
    {
784 2
        if (!($this instanceof metadata_interface)) {
785
            exception::no_metadata();
786
            return false;
787
        }
788 2
        return $this->metadata_isapproved;
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_isapproved does not exist on midgard\portable\api\mgdobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
789
    }
790
791 2
    public function unapprove() : bool
792
    {
793 2
        return $this->manage_meta_property("approve", false);
794
    }
795
796 3
    public function lock() : bool
797
    {
798 3
        if ($this->is_locked()) {
799 1
            exception::object_is_locked();
800 1
            return false;
801
        }
802 3
        return $this->manage_meta_property("lock", true);
803
    }
804
805 3
    public function is_locked() : bool
806
    {
807 3
        if (!($this instanceof metadata_interface)) {
808 1
            exception::no_metadata();
809 1
            return false;
810
        }
811 2
        return $this->metadata_islocked;
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_islocked does not exist on midgard\portable\api\mgdobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
812
    }
813
814 2
    public function unlock() : bool
815
    {
816 2
        return $this->manage_meta_property("lock", false);
817
    }
818
}
819