Passed
Push — master ( 44a3ca...abeffc )
by Laurent
01:59
created

app_TraceableRecordSet::isTraceable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
//-------------------------------------------------------------------------
3
// OVIDENTIA http://www.ovidentia.org
4
// Ovidentia is free software; you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation; either version 2, or (at your option)
7
// any later version.
8
//
9
// This program is distributed in the hope that it will be useful, but
10
// WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
// See the GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program; if not, write to the Free Software
16
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
17
// USA.
18
//-------------------------------------------------------------------------
19
/**
20
 * @license http://opensource.org/licenses/gpl-license.php GNU General Public License (GPL)
21
 * @copyright Copyright (c) 2018 by CANTICO ({@link http://www.cantico.fr})
22
 */
23
24
$App = app_App();
25
$App->includeRecordSet();
26
27
28
/**
29
 *
30
 * @property ORM_IntField       $createdBy
31
 * @property ORM_DateTimeField  $createdOn
32
 * @property ORM_IntField       $modifiedBy
33
 * @property ORM_DateTimeField  $modifiedOn
34
 * @property ORM_IntField       $deletedBy
35
 * @property ORM_DateTimeField  $deletedOn
36
 * @property ORM_BoolField      $deleted
37
 * @property ORM_StringField    $uuid
38
 *
39
 * @method app_TraceableRecord    get(mixed $criteria)
40
 * @method app_TraceableRecord    request(mixed $criteria)
41
 * @method app_TraceableRecord[]  select(\ORM_Criteria $criteria = null)
42
 * @method app_TraceableRecord    newRecord()
43
 */
44
class app_TraceableRecordSet extends app_RecordSet
45
{
46
    /**
47
     * @var bool
48
     */
49
    private $loggable = false;
50
51
    /**
52
     * @var bool
53
     */
54
    private $traceable = true;
55
56
    /**
57
     * @param Func_App $App
58
     */
59
    public function __construct(Func_App $App)
60
    {
61
        parent::__construct($App);
62
63
        $this->addFields(
64
            ORM_UserField('createdBy')
65
                ->setDescription('Created by'),
66
            ORM_DateTimeField('createdOn')
67
                ->setDescription('Created on'),
68
            ORM_UserField('modifiedBy')
69
                ->setDescription('Modified by'),
70
            ORM_DateTimeField('modifiedOn')
71
                ->setDescription('Modified on'),
72
            ORM_IntField('deletedBy')
73
                ->setDescription('Deleted by'),
74
            ORM_DateTimeField('deletedOn')
75
                ->setDescription('Deleted on'),
76
            ORM_BoolField('deleted')
77
                ->setDescription('Deleted'),
78
            ORM_StringField('uuid')
79
                ->setDescription('Universally Unique IDentifier')
80
81
        );
82
83
        // This condition will be applied whenever we select or join Records from this RecordSet.
84
        $this->setDefaultCriteria($this->deleted->is(false));
85
    }
86
87
88
    /**
89
     * Defines if the insertions/updates/deletions on the recordSet will be logged.
90
     *
91
     * @param bool $loggable
92
     * @return self
93
     */
94
    protected function setLoggable($loggable)
95
    {
96
        $this->loggable = $loggable;
97
        return $this;
98
    }
99
100
101
    /**
102
     * Checks if the insertions/updates/deletions on the recordSet will be logged.
103
     *
104
     * @return bool
105
     */
106
    protected function isLoggable()
107
    {
108
        return $this->loggable;
109
    }
110
111
112
113
114
    /**
115
     * Defines if the insertions/updates/deletions on the recordSet will be traced.
116
     *
117
     * @param bool $traceable
118
     * @return self
119
     */
120
    public function setTraceable($traceable)
121
    {
122
        $this->traceable = $traceable;
123
        return $this;
124
    }
125
126
127
    /**
128
     * Checks if the insertions/updates/deletions on the recordSet will be traced.
129
     *
130
     * @return bool
131
     */
132
    public function isTraceable()
133
    {
134
        return $this->traceable;
135
    }
136
137
138
    /**
139
     * @param app_TraceableRecord   $record
140
     * @param bool                  $noTrace
141
     */
142
    protected function logSave(app_TraceableRecord $record, $noTrace)
143
    {
144
        if (!$this->isLoggable()) {
145
            return;
146
        }
147
        $App = $this->App();
148
        $userId = bab_getUserId();
149
        $logSet = $App->LogSet();
150
        $log = $logSet->newRecord();
151
        $log->noTrace = $noTrace;
152
        $log->objectClass = get_class($record);
153
        $log->objectId = $record->id;
154
        $now = date('Y-m-d H:i:s');
155
        $log->modifiedOn = $now;
156
        $log->modifiedBy = $userId;
157
        $log->data = $logSet->serialize($record);
0 ignored issues
show
Documentation Bug introduced by
It seems like $logSet->serialize($record) of type array is incompatible with the declared type string of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
158
        $log->save();
159
    }
160
161
162
    /**
163
     * @param ORM_Criteria          $criteria
164
     * @param bool                  $noTrace
165
     */
166
    protected function logDelete(ORM_Criteria $criteria, $noTrace)
167
    {
168
        if (!$this->isLoggable()) {
169
            return;
170
        }
171
        $App = $this->App();
172
        $userId = bab_getUserId();
173
        $logSet = $App->LogSet();
174
        $deletedRecords = $this->select($criteria);
175
        foreach ($deletedRecords as $record) {
176
            $log = $logSet->newRecord();
177
            $log->noTrace = $noTrace;
178
            $log->objectClass = get_class($record);
179
            $log->objectId = $record->id;
180
            $now = date('Y-m-d H:i:s');
181
            $log->modifiedOn = $now;
182
            $log->modifiedBy = $userId;
183
            $log->data = '';
184
            $log->save();
185
        }
186
    }
187
188
    /**
189
     * Returns an iterator on records matching the specified criteria.
190
     * The iterator will not include records flagged as deleted unless
191
     * the $includeDeleted parameter is set to true.
192
     *
193
     * @param ORM_Criteria  $criteria       Criteria for selecting records.
194
     * @param bool          $includeDeleted True to include delete-flagged records.
195
     *
196
     * @return app_TraceableRecord[]        Iterator on success, null if the backend has not been set
197
     */
198
    public function select(ORM_Criteria $criteria = null, $includeDeleted = false)
199
    {
200
        if ($includeDeleted) {
201
            $this->setDefaultCriteria(null);
202
        }
203
        return parent::select($criteria);
204
    }
205
206
207
    /**
208
     * Returns the first item matching the specified criteria.
209
     * The item will not include records flagged as deleted unless
210
     * the $includeDeleted parameter is set to true.
211
     *
212
     * @param ORM_Criteria	$criteria			Criteria for selecting records.
213
     * @param string		$sPropertyName		The name of the property on which the value applies. If not specified or null, the set's primary key will be used.
214
     * @param bool			$includeDeleted		True to include delete-flagged records.
215
     *
216
     * @return ORM_Item				Iterator on success, null if the backend has not been set
217
     */
218
    /*public function get(ORM_Criteria $criteria = null, $sPropertyName = null, $includeDeleted = false)
219
    {
220
        if ($includeDeleted) {
221
            $this->setDefaultCriteria(null);
222
        }
223
        return parent::get($criteria, $sPropertyName);
224
    }*/
225
226
227
    /**
228
     * Deleted records matching the specified criteria.
229
     * If $definitive is false, records are not actually deleted
230
     * from the database but only flagged as so.
231
     *
232
     * @param ORM_Criteria	$criteria		The criteria for selecting the records to delete.
233
     * @param bool			$definitive		True to delete permanently the record.
234
     *
235
     * @return boolean				True on success, false otherwise
236
     */
237
    public function delete(ORM_Criteria $criteria = null, $definitive = false)
238
    {
239
        $definitive = $definitive || !$this->isTraceable();
240
        $this->logDelete($criteria, $definitive);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type null; however, parameter $criteria of app_TraceableRecordSet::logDelete() does only seem to accept ORM_Criteria, 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

240
        $this->logDelete(/** @scrutinizer ignore-type */ $criteria, $definitive);
Loading history...
241
        if ($definitive) {
242
            return parent::delete($criteria);
243
        }
244
245
        require_once $GLOBALS['babInstallPath'] . '/utilit/dateTime.php';
246
        $now = BAB_DateTime::now()->getIsoDateTime();
247
248
        $records = $this->select($criteria);
249
250
251
        foreach ($records as $record) {
252
            /* @var $record app_TraceableRecord */
253
            // Could be optimized at ORM level
254
            $record->deleted = true;
255
            $record->deletedOn = $now;
256
            $record->deletedBy = bab_getUserId();
257
            if (!parent::save($record)) {
258
                return false;
259
            }
260
        }
261
        return true;
262
    }
263
264
    /**
265
     * Saves a record and keeps traces of the user doing it.
266
     *
267
     * @param app_TraceableRecord	$record			The record to save.
268
     * @param bool					$noTrace		True to bypass the tracing of modifications.
269
     *
270
     * @return boolean				True on success, false otherwise
271
     */
272
    public function save(ORM_Record $record, $noTrace = false)
273
    {
274
        $noTrace = $noTrace || !$this->isTraceable();
275
        $this->logSave($record, $noTrace);
276
        if ($noTrace) {
277
            return parent::save($record);
278
        }
279
280
        require_once $GLOBALS['babInstallPath'] . '/utilit/dateTime.php';
281
282
        $now = BAB_DateTime::now()->getIsoDateTime();
283
284
        // We first check if the record already has a createdBy.
285
        $set = $record->getParentSet();
286
        $primaryKey = $set->getPrimaryKey();
287
288
        if (empty($record->{$primaryKey})) {
289
            $record->initValue('createdBy', bab_getUserId());
290
            $record->initValue('createdOn', $now);
291
            $record->initValue('uuid', $this->uuid());
292
        }
293
294
        $record->initValue('modifiedBy', bab_getUserId());
295
        $record->initValue('modifiedOn', $now);
296
297
        return parent::save($record);
298
    }
299
300
301
302
303
    /**
304
     * Generates a Universally Unique IDentifier, version 4.
305
     * RFC 4122 (http://www.ietf.org/rfc/rfc4122.txt)
306
     * @return string
307
     */
308
    private function uuid()
309
    {
310
        return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
311
            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
312
            mt_rand( 0, 0x0fff ) | 0x4000,
313
            mt_rand( 0, 0x3fff ) | 0x8000,
314
            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) );
315
    }
316
317
    /**
318
     * Get record by UUID or null if the record does not exists or is deleted or if the uuid is empty
319
     * @param	string	$uuid
320
     * @return app_TraceableRecord
321
     */
322
    public function getRecordByUuid($uuid)
323
    {
324
        if ('' === (string) $uuid) {
325
            return null;
326
        }
327
328
329
        $record = $this->get($this->uuid->is($uuid));
330
331
        if (!isset($record)) {
332
            return null;
333
        }
334
335
        if (!($record instanceOf app_TraceableRecord)) {
0 ignored issues
show
introduced by
$record is always a sub-type of app_TraceableRecord.
Loading history...
336
            return null;
337
        }
338
339
        if ($record->deleted) {
340
            return null;
341
        }
342
343
        return $record;
344
    }
345
346
347
348
349
350
351
352
    /**
353
     * Returns an iterator of traceableRecord linked to the specified source,
354
     * optionally filtered on the specified link type.
355
     *
356
     * @param	app_Record | array		$source			source can be an array of record
357
     * @param	string					$linkType
358
     *
359
     * @return ORM_Iterator
360
     */
361
    public function selectLinkedTo($source, $linkType = null)
362
    {
363
        $linkSet = $this->App()->LinkSet();
364
        if (is_array($source) || ($source instanceof Iterator)) {
365
            return $linkSet->selectForSources($source, $this->getRecordClassName(), $linkType);
366
        } else {
367
            return $linkSet->selectForSource($source, $this->getRecordClassName(), $linkType);
368
        }
369
    }
370
371
    /**
372
     * Returns an iterator of traceableRecord linked to the specified target,
373
     * optionally filtered on the specified link type.
374
     *
375
     * @param	app_Record | array		$target			target can be an array of record
376
     * @param	string					$linkType
377
     *
378
     * @return ORM_Iterator
379
     */
380
    public function selectLinkedFrom($target, $linkType = null)
381
    {
382
        $linkSet = $this->App()->LinkSet();
383
        if (is_array($target) || ($target instanceof Iterator)) {
384
            return $linkSet->selectForTargets($target, $this->getRecordClassName(), $linkType);
385
        } else {
386
            return $linkSet->selectForTarget($target, $this->getRecordClassName(), $linkType);
387
        }
388
    }
389
390
391
    /**
392
     * Returns a criteria usable to select records of this set which are source of app_Links to the specified app_Record.
393
     *
394
     * @param app_Record $target
395
     * @param string     $linkType
396
     * @return ORM_Criteria
397
     */
398
    public function isSourceOf(app_Record $target, $linkType = null)
399
    {
400
        $linkSet = $this->App()->LinkSet();
401
402
        $linkSet->hasOne('sourceId', get_class($this));
403
404
        $criteria =	$linkSet->targetClass->is(get_class($target))
405
            ->_AND_($linkSet->targetId->is($target->id))
406
            ->_AND_($linkSet->sourceClass->is($this->getRecordClassName()));
407
        if (isset($linkType)) {
408
            if (is_array($linkType)) {
0 ignored issues
show
introduced by
The condition is_array($linkType) is always false.
Loading history...
409
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
410
            } else {
411
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
412
            }
413
        }
414
        $criteria = $this->id->in($criteria);
415
416
        return $criteria;
417
    }
418
419
420
421
    /**
422
     * Returns a criteria usable to select records of this set which are target of app_Links to the specified app_Record.
423
     *
424
     * @param app_Record            $source
425
     * @param null|string|string[]  $linkType
426
     * @return ORM_Criteria
427
     */
428
    public function isTargetOf(app_Record $source, $linkType = null)
429
    {
430
        $linkSet = $this->App()->LinkSet();
431
432
        $linkSet->hasOne('targetId', get_class($this));
433
434
        $criteria =	$linkSet->sourceClass->is(get_class($source))
435
            ->_AND_($linkSet->sourceId->is($source->id))
436
            ->_AND_($linkSet->targetClass->is($this->getRecordClassName()));
437
        if (isset($linkType)) {
438
            if (is_array($linkType)) {
439
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
440
            } else {
441
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
442
            }
443
        }
444
        $criteria = $this->id->in($criteria);
445
446
        return $criteria;
447
    }
448
449
450
    /**
451
     * Returns a criteria usable to select records of this set which are target of app_Links from the specified app_Records.
452
     *
453
     * @since 1.0.23
454
     *
455
     * @param app_Record[]          $sources
456
     * @param null|string|string[]  $linkType
457
     * @return ORM_Criteria
458
     */
459
    public function isTargetOfAny($sources, $linkType = null)
460
    {
461
        $linkSet = $this->App()->LinkSet();
462
        $linkSet->hasOne('targetId', get_class($this));
463
464
        $sourceIdsByClasses = array();
465
        foreach ($sources as $source) {
466
            $sourceClass = get_class($source);
467
            if (!isset($sourceIdsByClasses[$sourceClass])) {
468
                $sourceIdsByClasses[$sourceClass] = array();
469
            }
470
            $sourceIdsByClasses[$sourceClass][] = $source->id;
471
        }
472
473
        $sourcesCriteria = array();
474
        foreach ($sourceIdsByClasses as $sourceClass => $sourceIds) {
475
            $sourcesCriteria[] = $linkSet->sourceClass->is($sourceClass)->_AND_($linkSet->sourceId->in($sourceIds));
476
        }
477
478
        $criteria =	$linkSet->all(
479
            $linkSet->targetClass->is($this->getRecordClassName()),
480
            $linkSet->any($sourcesCriteria)
481
        );
482
        if (isset($linkType)) {
483
            if (is_array($linkType)) {
484
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
485
            } else {
486
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
487
            }
488
        }
489
        $criteria = $this->id->in($criteria);
490
491
        return $criteria;
492
    }
493
494
495
    /**
496
     * Returns a criteria usable to select records of this set which are source of app_Links to the specified app_Records.
497
     *
498
     * @since 1.0.23
499
     *
500
     * @param app_Record[]     $targets
501
     * @param string|null      $linkType
502
     * @return ORM_Criteria
503
     */
504
    public function isSourceOfAny($targets, $linkType = null)
505
    {
506
        $linkSet = $this->App()->LinkSet();
507
        $linkSet->hasOne('sourceId', get_class($this));
508
509
        $targetIdsByClasses = array();
510
        foreach ($targets as $target) {
511
            $targetClass = get_class($target);
512
            if (!isset($targetIdsByClasses[$targetClass])) {
513
                $targetIdsByClasses[$targetClass] = array();
514
            }
515
            $targetIdsByClasses[$targetClass][] = $target->id;
516
        }
517
518
        $targetsCriteria = array();
519
        foreach ($targetIdsByClasses as $targetClass => $targetIds) {
520
            $targetsCriteria[] = $linkSet->targetClass->is($targetClass)->_AND_($linkSet->targetId->in($targetIds));
521
        }
522
523
        $criteria =	$linkSet->all(
524
            $linkSet->sourceClass->is($this->getRecordClassName()),
525
            $linkSet->any($targetsCriteria)
526
        );
527
        if (isset($linkType)) {
528
            if (is_array($linkType)) {
0 ignored issues
show
introduced by
The condition is_array($linkType) is always false.
Loading history...
529
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
530
            } else {
531
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
532
            }
533
        }
534
        $criteria = $this->id->in($criteria);
535
536
        return $criteria;
537
    }
538
539
540
    /**
541
     * Match records created by the specified user or the current connected user if none specified.
542
     *
543
	 * @param int|null $userId
544
	 *
545
	 * @return ORM_IsCriterion
546
	 */
547
    public function isOwn($userId = null)
548
    {
549
        if (!isset($userId)) {
550
            $userId = bab_getUserId();
551
        }
552
        return $this->createdBy->is($userId);
553
    }
554
555
}
556
557
558
/**
559
 * A traceable record automatically stores by whom and when it was created,
560
 * modified and even deleted.
561
 *
562
 * By default "deleted" records (through the standard delete() methods)
563
 * actually stay in the database and are only flagged as deleted.
564
 *
565
 * @property int        $createdBy
566
 * @property string     $createdOn
567
 * @property int        $modifiedBy
568
 * @property string     $modifiedOn
569
 * @property int        $deletedBy
570
 * @property string     $deletedOn
571
 * @property string     $uuid
572
 * @property bool       $deleted
573
 */
574
class app_TraceableRecord extends app_Record
575
{
576
    /**
577
     * Saves the record.
578
     *
579
     * @param bool  $noTrace        True to bypass the tracing of modifications.
580
     *
581
     * @return bool True on success, false otherwise
582
     */
583
    public function save($noTrace = false)
584
    {
585
        return $this->getParentSet()->save($this, $noTrace);
0 ignored issues
show
Unused Code introduced by
The call to ORM_RecordSet::save() has too many arguments starting with $noTrace. ( Ignorable by Annotation )

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

585
        return $this->getParentSet()->/** @scrutinizer ignore-call */ save($this, $noTrace);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
586
    }
587
588
589
590
    /**
591
     * @return bool
592
     */
593
    public function isLinkedTo(app_Record $source, $linkType = null)
594
    {
595
        $linkSet = $this->App()->LinkSet();
596
597
        $criteria =	$linkSet->sourceClass->is(get_class($source))
598
            ->_AND_($linkSet->sourceId->is($source->id))
599
            ->_AND_($linkSet->targetClass->is(get_class($this)))
600
            ->_AND_($linkSet->targetId->is($this->id));
601
        if (isset($linkType)) {
602
            if (is_array($linkType)) {
603
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
604
            } else {
605
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
606
            }
607
        }
608
        $links = $linkSet->select($criteria);
0 ignored issues
show
Unused Code introduced by
The call to app_LinkSet::select() has too many arguments starting with $criteria. ( Ignorable by Annotation )

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

608
        /** @scrutinizer ignore-call */ 
609
        $links = $linkSet->select($criteria);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
609
610
        $isLinked = ($links->count() > 0);
611
612
        $linkSet->__destruct();
613
        unset($linkSet);
614
615
        return $isLinked;
616
    }
617
618
619
620
    /**
621
     * @return bool
622
     */
623
    public function isSourceOf(app_Record $target, $linkType = null)
624
    {
625
        $linkSet = $this->App()->LinkSet();
626
627
        $criteria =	$linkSet->targetClass->is(get_class($target))
628
            ->_AND_($linkSet->targetId->is($target->id))
629
            ->_AND_($linkSet->sourceClass->is(get_class($this)))
630
            ->_AND_($linkSet->sourceId->is($this->id));
631
        if (isset($linkType)) {
632
            if (is_array($linkType)) {
633
                $criteria = $criteria->_AND_($linkSet->type->in($linkType));
634
            } else {
635
                $criteria = $criteria->_AND_($linkSet->type->is($linkType));
636
            }
637
        }
638
        $links = $linkSet->select($criteria);
0 ignored issues
show
Unused Code introduced by
The call to app_LinkSet::select() has too many arguments starting with $criteria. ( Ignorable by Annotation )

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

638
        /** @scrutinizer ignore-call */ 
639
        $links = $linkSet->select($criteria);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
639
640
        $isLinked = ($links->count() > 0);
641
642
        $linkSet->__destruct();
643
        unset($linkSet);
644
645
        return $isLinked;
646
    }
647
648
649
650
    /**
651
     * Link record to $source
652
     *
653
     * @return app_TraceableRecord
654
     */
655
    public function linkTo(app_Record $source, $linkType = '')
656
    {
657
        $linkSet = $this->App()->LinkSet();
658
        /* @var $link app_Link */
659
        $link = $linkSet->newRecord();
660
        $link->sourceClass = get_class($source);
661
        $link->sourceId = $source->id;
662
        $link->targetClass = get_class($this);
663
        $link->targetId = $this->id;
664
        $link->type = $linkType;
665
        $link->save();
666
667
        $link->__destruct();
668
        unset($link);
669
670
        $linkSet->__destruct();
671
        unset($linkSet);
672
673
        return $this;
674
    }
675
}
676