CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Team
    • Issues (Github)
    • YouTube Channel
    • Get Involved
    • Bakery
    • Featured Resources
    • Newsletter
    • Certification
    • My CakePHP
    • CakeFest
    • Facebook
    • Twitter
    • Help & Support
    • Forum
    • Stack Overflow
    • IRC
    • Slack
    • Paid Support
CakePHP

C CakePHP 3.7 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.7
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Namespaces

  • Cake
    • Auth
      • Storage
    • Cache
      • Engine
    • Collection
      • Iterator
    • Command
    • Console
      • Exception
    • Controller
      • Component
      • Exception
    • Core
      • Configure
        • Engine
      • Exception
      • Retry
    • Database
      • Driver
      • Exception
      • Expression
      • Schema
      • Statement
      • Type
    • Datasource
      • Exception
    • Error
      • Middleware
    • Event
      • Decorator
    • Filesystem
    • Form
    • Http
      • Client
        • Adapter
        • Auth
      • Cookie
      • Exception
      • Middleware
      • Session
    • I18n
      • Formatter
      • Middleware
      • Parser
    • Log
      • Engine
    • Mailer
      • Exception
      • Transport
    • Network
      • Exception
    • ORM
      • Association
      • Behavior
        • Translate
      • Exception
      • Locator
      • Rule
    • Routing
      • Exception
      • Filter
      • Middleware
      • Route
    • Shell
      • Helper
      • Task
    • TestSuite
      • Fixture
      • Stub
    • Utility
      • Exception
    • Validation
    • View
      • Exception
      • Form
      • Helper
      • Widget
  • None

Classes

  • BelongsTo
  • BelongsToMany
  • HasMany
  • HasOne

Traits

  • DependentDeleteTrait
   1: <?php
   2: /**
   3:  * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
   4:  * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
   5:  *
   6:  * Licensed under The MIT License
   7:  * For full copyright and license information, please see the LICENSE.txt
   8:  * Redistributions of files must retain the above copyright notice.
   9:  *
  10:  * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11:  * @link          https://cakephp.org CakePHP(tm) Project
  12:  * @since         3.0.0
  13:  * @license       https://opensource.org/licenses/mit-license.php MIT License
  14:  */
  15: namespace Cake\ORM\Association;
  16: 
  17: use Cake\Core\App;
  18: use Cake\Database\ExpressionInterface;
  19: use Cake\Database\Expression\IdentifierExpression;
  20: use Cake\Datasource\EntityInterface;
  21: use Cake\Datasource\QueryInterface;
  22: use Cake\ORM\Association;
  23: use Cake\ORM\Association\Loader\SelectWithPivotLoader;
  24: use Cake\ORM\Query;
  25: use Cake\ORM\Table;
  26: use Cake\Utility\Inflector;
  27: use InvalidArgumentException;
  28: use SplObjectStorage;
  29: use Traversable;
  30: 
  31: /**
  32:  * Represents an M - N relationship where there exists a junction - or join - table
  33:  * that contains the association fields between the source and the target table.
  34:  *
  35:  * An example of a BelongsToMany association would be Article belongs to many Tags.
  36:  */
  37: class BelongsToMany extends Association
  38: {
  39: 
  40:     /**
  41:      * Saving strategy that will only append to the links set
  42:      *
  43:      * @var string
  44:      */
  45:     const SAVE_APPEND = 'append';
  46: 
  47:     /**
  48:      * Saving strategy that will replace the links with the provided set
  49:      *
  50:      * @var string
  51:      */
  52:     const SAVE_REPLACE = 'replace';
  53: 
  54:     /**
  55:      * The type of join to be used when adding the association to a query
  56:      *
  57:      * @var string
  58:      */
  59:     protected $_joinType = QueryInterface::JOIN_TYPE_INNER;
  60: 
  61:     /**
  62:      * The strategy name to be used to fetch associated records.
  63:      *
  64:      * @var string
  65:      */
  66:     protected $_strategy = self::STRATEGY_SELECT;
  67: 
  68:     /**
  69:      * Junction table instance
  70:      *
  71:      * @var \Cake\ORM\Table
  72:      */
  73:     protected $_junctionTable;
  74: 
  75:     /**
  76:      * Junction table name
  77:      *
  78:      * @var string
  79:      */
  80:     protected $_junctionTableName;
  81: 
  82:     /**
  83:      * The name of the hasMany association from the target table
  84:      * to the junction table
  85:      *
  86:      * @var string
  87:      */
  88:     protected $_junctionAssociationName;
  89: 
  90:     /**
  91:      * The name of the property to be set containing data from the junction table
  92:      * once a record from the target table is hydrated
  93:      *
  94:      * @var string
  95:      */
  96:     protected $_junctionProperty = '_joinData';
  97: 
  98:     /**
  99:      * Saving strategy to be used by this association
 100:      *
 101:      * @var string
 102:      */
 103:     protected $_saveStrategy = self::SAVE_REPLACE;
 104: 
 105:     /**
 106:      * The name of the field representing the foreign key to the target table
 107:      *
 108:      * @var string|array
 109:      */
 110:     protected $_targetForeignKey;
 111: 
 112:     /**
 113:      * The table instance for the junction relation.
 114:      *
 115:      * @var string|\Cake\ORM\Table
 116:      */
 117:     protected $_through;
 118: 
 119:     /**
 120:      * Valid strategies for this type of association
 121:      *
 122:      * @var array
 123:      */
 124:     protected $_validStrategies = [
 125:         self::STRATEGY_SELECT,
 126:         self::STRATEGY_SUBQUERY
 127:     ];
 128: 
 129:     /**
 130:      * Whether the records on the joint table should be removed when a record
 131:      * on the source table is deleted.
 132:      *
 133:      * Defaults to true for backwards compatibility.
 134:      *
 135:      * @var bool
 136:      */
 137:     protected $_dependent = true;
 138: 
 139:     /**
 140:      * Filtered conditions that reference the target table.
 141:      *
 142:      * @var null|array
 143:      */
 144:     protected $_targetConditions;
 145: 
 146:     /**
 147:      * Filtered conditions that reference the junction table.
 148:      *
 149:      * @var null|array
 150:      */
 151:     protected $_junctionConditions;
 152: 
 153:     /**
 154:      * Order in which target records should be returned
 155:      *
 156:      * @var mixed
 157:      */
 158:     protected $_sort;
 159: 
 160:     /**
 161:      * Sets the name of the field representing the foreign key to the target table.
 162:      *
 163:      * @param string $key the key to be used to link both tables together
 164:      * @return $this
 165:      */
 166:     public function setTargetForeignKey($key)
 167:     {
 168:         $this->_targetForeignKey = $key;
 169: 
 170:         return $this;
 171:     }
 172: 
 173:     /**
 174:      * Gets the name of the field representing the foreign key to the target table.
 175:      *
 176:      * @return string
 177:      */
 178:     public function getTargetForeignKey()
 179:     {
 180:         if ($this->_targetForeignKey === null) {
 181:             $this->_targetForeignKey = $this->_modelKey($this->getTarget()->getAlias());
 182:         }
 183: 
 184:         return $this->_targetForeignKey;
 185:     }
 186: 
 187:     /**
 188:      * Sets the name of the field representing the foreign key to the target table.
 189:      * If no parameters are passed current field is returned
 190:      *
 191:      * @deprecated 3.4.0 Use setTargetForeignKey()/getTargetForeignKey() instead.
 192:      * @param string|null $key the key to be used to link both tables together
 193:      * @return string
 194:      */
 195:     public function targetForeignKey($key = null)
 196:     {
 197:         deprecationWarning(
 198:             'BelongToMany::targetForeignKey() is deprecated. ' .
 199:             'Use setTargetForeignKey()/getTargetForeignKey() instead.'
 200:         );
 201:         if ($key !== null) {
 202:             $this->setTargetForeignKey($key);
 203:         }
 204: 
 205:         return $this->getTargetForeignKey();
 206:     }
 207: 
 208:     /**
 209:      * Whether this association can be expressed directly in a query join
 210:      *
 211:      * @param array $options custom options key that could alter the return value
 212:      * @return bool if the 'matching' key in $option is true then this function
 213:      * will return true, false otherwise
 214:      */
 215:     public function canBeJoined(array $options = [])
 216:     {
 217:         return !empty($options['matching']);
 218:     }
 219: 
 220:     /**
 221:      * Gets the name of the field representing the foreign key to the source table.
 222:      *
 223:      * @return string
 224:      */
 225:     public function getForeignKey()
 226:     {
 227:         if ($this->_foreignKey === null) {
 228:             $this->_foreignKey = $this->_modelKey($this->getSource()->getTable());
 229:         }
 230: 
 231:         return $this->_foreignKey;
 232:     }
 233: 
 234:     /**
 235:      * Sets the sort order in which target records should be returned.
 236:      *
 237:      * @param mixed $sort A find() compatible order clause
 238:      * @return $this
 239:      */
 240:     public function setSort($sort)
 241:     {
 242:         $this->_sort = $sort;
 243: 
 244:         return $this;
 245:     }
 246: 
 247:     /**
 248:      * Gets the sort order in which target records should be returned.
 249:      *
 250:      * @return mixed
 251:      */
 252:     public function getSort()
 253:     {
 254:         return $this->_sort;
 255:     }
 256: 
 257:     /**
 258:      * Sets the sort order in which target records should be returned.
 259:      * If no arguments are passed the currently configured value is returned
 260:      *
 261:      * @deprecated 3.5.0 Use setSort()/getSort() instead.
 262:      * @param mixed $sort A find() compatible order clause
 263:      * @return mixed
 264:      */
 265:     public function sort($sort = null)
 266:     {
 267:         deprecationWarning(
 268:             'BelongToMany::sort() is deprecated. ' .
 269:             'Use setSort()/getSort() instead.'
 270:         );
 271:         if ($sort !== null) {
 272:             $this->setSort($sort);
 273:         }
 274: 
 275:         return $this->getSort();
 276:     }
 277: 
 278:     /**
 279:      * {@inheritDoc}
 280:      */
 281:     public function defaultRowValue($row, $joined)
 282:     {
 283:         $sourceAlias = $this->getSource()->getAlias();
 284:         if (isset($row[$sourceAlias])) {
 285:             $row[$sourceAlias][$this->getProperty()] = $joined ? null : [];
 286:         }
 287: 
 288:         return $row;
 289:     }
 290: 
 291:     /**
 292:      * Sets the table instance for the junction relation. If no arguments
 293:      * are passed, the current configured table instance is returned
 294:      *
 295:      * @param string|\Cake\ORM\Table|null $table Name or instance for the join table
 296:      * @return \Cake\ORM\Table
 297:      */
 298:     public function junction($table = null)
 299:     {
 300:         if ($table === null && $this->_junctionTable) {
 301:             return $this->_junctionTable;
 302:         }
 303: 
 304:         $tableLocator = $this->getTableLocator();
 305:         if ($table === null && $this->_through) {
 306:             $table = $this->_through;
 307:         } elseif ($table === null) {
 308:             $tableName = $this->_junctionTableName();
 309:             $tableAlias = Inflector::camelize($tableName);
 310: 
 311:             $config = [];
 312:             if (!$tableLocator->exists($tableAlias)) {
 313:                 $config = ['table' => $tableName];
 314: 
 315:                 // Propagate the connection if we'll get an auto-model
 316:                 if (!App::className($tableAlias, 'Model/Table', 'Table')) {
 317:                     $config['connection'] = $this->getSource()->getConnection();
 318:                 }
 319:             }
 320:             $table = $tableLocator->get($tableAlias, $config);
 321:         }
 322: 
 323:         if (is_string($table)) {
 324:             $table = $tableLocator->get($table);
 325:         }
 326:         $source = $this->getSource();
 327:         $target = $this->getTarget();
 328: 
 329:         $this->_generateSourceAssociations($table, $source);
 330:         $this->_generateTargetAssociations($table, $source, $target);
 331:         $this->_generateJunctionAssociations($table, $source, $target);
 332: 
 333:         return $this->_junctionTable = $table;
 334:     }
 335: 
 336:     /**
 337:      * Generate reciprocal associations as necessary.
 338:      *
 339:      * Generates the following associations:
 340:      *
 341:      * - target hasMany junction e.g. Articles hasMany ArticlesTags
 342:      * - target belongsToMany source e.g Articles belongsToMany Tags.
 343:      *
 344:      * You can override these generated associations by defining associations
 345:      * with the correct aliases.
 346:      *
 347:      * @param \Cake\ORM\Table $junction The junction table.
 348:      * @param \Cake\ORM\Table $source The source table.
 349:      * @param \Cake\ORM\Table $target The target table.
 350:      * @return void
 351:      */
 352:     protected function _generateTargetAssociations($junction, $source, $target)
 353:     {
 354:         $junctionAlias = $junction->getAlias();
 355:         $sAlias = $source->getAlias();
 356: 
 357:         if (!$target->hasAssociation($junctionAlias)) {
 358:             $target->hasMany($junctionAlias, [
 359:                 'targetTable' => $junction,
 360:                 'foreignKey' => $this->getTargetForeignKey(),
 361:                 'strategy' => $this->_strategy,
 362:             ]);
 363:         }
 364:         if (!$target->hasAssociation($sAlias)) {
 365:             $target->belongsToMany($sAlias, [
 366:                 'sourceTable' => $target,
 367:                 'targetTable' => $source,
 368:                 'foreignKey' => $this->getTargetForeignKey(),
 369:                 'targetForeignKey' => $this->getForeignKey(),
 370:                 'through' => $junction,
 371:                 'conditions' => $this->getConditions(),
 372:                 'strategy' => $this->_strategy,
 373:             ]);
 374:         }
 375:     }
 376: 
 377:     /**
 378:      * Generate additional source table associations as necessary.
 379:      *
 380:      * Generates the following associations:
 381:      *
 382:      * - source hasMany junction e.g. Tags hasMany ArticlesTags
 383:      *
 384:      * You can override these generated associations by defining associations
 385:      * with the correct aliases.
 386:      *
 387:      * @param \Cake\ORM\Table $junction The junction table.
 388:      * @param \Cake\ORM\Table $source The source table.
 389:      * @return void
 390:      */
 391:     protected function _generateSourceAssociations($junction, $source)
 392:     {
 393:         $junctionAlias = $junction->getAlias();
 394:         if (!$source->hasAssociation($junctionAlias)) {
 395:             $source->hasMany($junctionAlias, [
 396:                 'targetTable' => $junction,
 397:                 'foreignKey' => $this->getForeignKey(),
 398:                 'strategy' => $this->_strategy,
 399:             ]);
 400:         }
 401:     }
 402: 
 403:     /**
 404:      * Generate associations on the junction table as necessary
 405:      *
 406:      * Generates the following associations:
 407:      *
 408:      * - junction belongsTo source e.g. ArticlesTags belongsTo Tags
 409:      * - junction belongsTo target e.g. ArticlesTags belongsTo Articles
 410:      *
 411:      * You can override these generated associations by defining associations
 412:      * with the correct aliases.
 413:      *
 414:      * @param \Cake\ORM\Table $junction The junction table.
 415:      * @param \Cake\ORM\Table $source The source table.
 416:      * @param \Cake\ORM\Table $target The target table.
 417:      * @return void
 418:      */
 419:     protected function _generateJunctionAssociations($junction, $source, $target)
 420:     {
 421:         $tAlias = $target->getAlias();
 422:         $sAlias = $source->getAlias();
 423: 
 424:         if (!$junction->hasAssociation($tAlias)) {
 425:             $junction->belongsTo($tAlias, [
 426:                 'foreignKey' => $this->getTargetForeignKey(),
 427:                 'targetTable' => $target
 428:             ]);
 429:         }
 430:         if (!$junction->hasAssociation($sAlias)) {
 431:             $junction->belongsTo($sAlias, [
 432:                 'foreignKey' => $this->getForeignKey(),
 433:                 'targetTable' => $source
 434:             ]);
 435:         }
 436:     }
 437: 
 438:     /**
 439:      * Alters a Query object to include the associated target table data in the final
 440:      * result
 441:      *
 442:      * The options array accept the following keys:
 443:      *
 444:      * - includeFields: Whether to include target model fields in the result or not
 445:      * - foreignKey: The name of the field to use as foreign key, if false none
 446:      *   will be used
 447:      * - conditions: array with a list of conditions to filter the join with
 448:      * - fields: a list of fields in the target table to include in the result
 449:      * - type: The type of join to be used (e.g. INNER)
 450:      *
 451:      * @param \Cake\ORM\Query $query the query to be altered to include the target table data
 452:      * @param array $options Any extra options or overrides to be taken in account
 453:      * @return void
 454:      */
 455:     public function attachTo(Query $query, array $options = [])
 456:     {
 457:         if (!empty($options['negateMatch'])) {
 458:             $this->_appendNotMatching($query, $options);
 459: 
 460:             return;
 461:         }
 462: 
 463:         $junction = $this->junction();
 464:         $belongsTo = $junction->getAssociation($this->getSource()->getAlias());
 465:         $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]);
 466:         $cond += $this->junctionConditions();
 467: 
 468:         $includeFields = null;
 469:         if (isset($options['includeFields'])) {
 470:             $includeFields = $options['includeFields'];
 471:         }
 472: 
 473:         // Attach the junction table as well we need it to populate _joinData.
 474:         $assoc = $this->_targetTable->getAssociation($junction->getAlias());
 475:         $newOptions = array_intersect_key($options, ['joinType' => 1, 'fields' => 1]);
 476:         $newOptions += [
 477:             'conditions' => $cond,
 478:             'includeFields' => $includeFields,
 479:             'foreignKey' => false,
 480:         ];
 481:         $assoc->attachTo($query, $newOptions);
 482:         $query->getEagerLoader()->addToJoinsMap($junction->getAlias(), $assoc, true);
 483: 
 484:         parent::attachTo($query, $options);
 485: 
 486:         $foreignKey = $this->getTargetForeignKey();
 487:         $thisJoin = $query->clause('join')[$this->getName()];
 488:         $thisJoin['conditions']->add($assoc->_joinCondition(['foreignKey' => $foreignKey]));
 489:     }
 490: 
 491:     /**
 492:      * {@inheritDoc}
 493:      */
 494:     protected function _appendNotMatching($query, $options)
 495:     {
 496:         if (empty($options['negateMatch'])) {
 497:             return;
 498:         }
 499:         if (!isset($options['conditions'])) {
 500:             $options['conditions'] = [];
 501:         }
 502:         $junction = $this->junction();
 503:         $belongsTo = $junction->getAssociation($this->getSource()->getAlias());
 504:         $conds = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]);
 505: 
 506:         $subquery = $this->find()
 507:             ->select(array_values($conds))
 508:             ->where($options['conditions'])
 509:             ->andWhere($this->junctionConditions());
 510: 
 511:         if (!empty($options['queryBuilder'])) {
 512:             $subquery = $options['queryBuilder']($subquery);
 513:         }
 514: 
 515:         $assoc = $junction->getAssociation($this->getTarget()->getAlias());
 516:         $conditions = $assoc->_joinCondition([
 517:             'foreignKey' => $this->getTargetForeignKey()
 518:         ]);
 519:         $subquery = $this->_appendJunctionJoin($subquery, $conditions);
 520: 
 521:         $query
 522:             ->andWhere(function ($exp) use ($subquery, $conds) {
 523:                 $identifiers = [];
 524:                 foreach (array_keys($conds) as $field) {
 525:                     $identifiers[] = new IdentifierExpression($field);
 526:                 }
 527:                 $identifiers = $subquery->newExpr()->add($identifiers)->setConjunction(',');
 528:                 $nullExp = clone $exp;
 529: 
 530:                 return $exp
 531:                     ->or_([
 532:                         $exp->notIn($identifiers, $subquery),
 533:                         $nullExp->and(array_map([$nullExp, 'isNull'], array_keys($conds))),
 534:                     ]);
 535:             });
 536:     }
 537: 
 538:     /**
 539:      * Get the relationship type.
 540:      *
 541:      * @return string
 542:      */
 543:     public function type()
 544:     {
 545:         return self::MANY_TO_MANY;
 546:     }
 547: 
 548:     /**
 549:      * Return false as join conditions are defined in the junction table
 550:      *
 551:      * @param array $options list of options passed to attachTo method
 552:      * @return bool false
 553:      */
 554:     protected function _joinCondition($options)
 555:     {
 556:         return false;
 557:     }
 558: 
 559:     /**
 560:      * {@inheritDoc}
 561:      *
 562:      * @return \Closure
 563:      */
 564:     public function eagerLoader(array $options)
 565:     {
 566:         $name = $this->_junctionAssociationName();
 567:         $loader = new SelectWithPivotLoader([
 568:             'alias' => $this->getAlias(),
 569:             'sourceAlias' => $this->getSource()->getAlias(),
 570:             'targetAlias' => $this->getTarget()->getAlias(),
 571:             'foreignKey' => $this->getForeignKey(),
 572:             'bindingKey' => $this->getBindingKey(),
 573:             'strategy' => $this->getStrategy(),
 574:             'associationType' => $this->type(),
 575:             'sort' => $this->getSort(),
 576:             'junctionAssociationName' => $name,
 577:             'junctionProperty' => $this->_junctionProperty,
 578:             'junctionAssoc' => $this->getTarget()->getAssociation($name),
 579:             'junctionConditions' => $this->junctionConditions(),
 580:             'finder' => function () {
 581:                 return $this->_appendJunctionJoin($this->find(), []);
 582:             }
 583:         ]);
 584: 
 585:         return $loader->buildEagerLoader($options);
 586:     }
 587: 
 588:     /**
 589:      * Clear out the data in the junction table for a given entity.
 590:      *
 591:      * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascading delete.
 592:      * @param array $options The options for the original delete.
 593:      * @return bool Success.
 594:      */
 595:     public function cascadeDelete(EntityInterface $entity, array $options = [])
 596:     {
 597:         if (!$this->getDependent()) {
 598:             return true;
 599:         }
 600:         $foreignKey = (array)$this->getForeignKey();
 601:         $bindingKey = (array)$this->getBindingKey();
 602:         $conditions = [];
 603: 
 604:         if (!empty($bindingKey)) {
 605:             $conditions = array_combine($foreignKey, $entity->extract($bindingKey));
 606:         }
 607: 
 608:         $table = $this->junction();
 609:         $hasMany = $this->getSource()->getAssociation($table->getAlias());
 610:         if ($this->_cascadeCallbacks) {
 611:             foreach ($hasMany->find('all')->where($conditions)->all()->toList() as $related) {
 612:                 $table->delete($related, $options);
 613:             }
 614: 
 615:             return true;
 616:         }
 617: 
 618:         $conditions = array_merge($conditions, $hasMany->getConditions());
 619: 
 620:         $table->deleteAll($conditions);
 621: 
 622:         return true;
 623:     }
 624: 
 625:     /**
 626:      * Returns boolean true, as both of the tables 'own' rows in the other side
 627:      * of the association via the joint table.
 628:      *
 629:      * @param \Cake\ORM\Table $side The potential Table with ownership
 630:      * @return bool
 631:      */
 632:     public function isOwningSide(Table $side)
 633:     {
 634:         return true;
 635:     }
 636: 
 637:     /**
 638:      * Sets the strategy that should be used for saving.
 639:      *
 640:      * @param string $strategy the strategy name to be used
 641:      * @throws \InvalidArgumentException if an invalid strategy name is passed
 642:      * @return $this
 643:      */
 644:     public function setSaveStrategy($strategy)
 645:     {
 646:         if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE])) {
 647:             $msg = sprintf('Invalid save strategy "%s"', $strategy);
 648:             throw new InvalidArgumentException($msg);
 649:         }
 650: 
 651:         $this->_saveStrategy = $strategy;
 652: 
 653:         return $this;
 654:     }
 655: 
 656:     /**
 657:      * Gets the strategy that should be used for saving.
 658:      *
 659:      * @return string the strategy to be used for saving
 660:      */
 661:     public function getSaveStrategy()
 662:     {
 663:         return $this->_saveStrategy;
 664:     }
 665: 
 666:     /**
 667:      * Sets the strategy that should be used for saving. If called with no
 668:      * arguments, it will return the currently configured strategy
 669:      *
 670:      * @deprecated 3.4.0 Use setSaveStrategy()/getSaveStrategy() instead.
 671:      * @param string|null $strategy the strategy name to be used
 672:      * @throws \InvalidArgumentException if an invalid strategy name is passed
 673:      * @return string the strategy to be used for saving
 674:      */
 675:     public function saveStrategy($strategy = null)
 676:     {
 677:         deprecationWarning(
 678:             'BelongsToMany::saveStrategy() is deprecated. ' .
 679:             'Use setSaveStrategy()/getSaveStrategy() instead.'
 680:         );
 681:         if ($strategy !== null) {
 682:             $this->setSaveStrategy($strategy);
 683:         }
 684: 
 685:         return $this->getSaveStrategy();
 686:     }
 687: 
 688:     /**
 689:      * Takes an entity from the source table and looks if there is a field
 690:      * matching the property name for this association. The found entity will be
 691:      * saved on the target table for this association by passing supplied
 692:      * `$options`
 693:      *
 694:      * When using the 'append' strategy, this function will only create new links
 695:      * between each side of this association. It will not destroy existing ones even
 696:      * though they may not be present in the array of entities to be saved.
 697:      *
 698:      * When using the 'replace' strategy, existing links will be removed and new links
 699:      * will be created in the joint table. If there exists links in the database to some
 700:      * of the entities intended to be saved by this method, they will be updated,
 701:      * not deleted.
 702:      *
 703:      * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
 704:      * @param array $options options to be passed to the save method in the target table
 705:      * @throws \InvalidArgumentException if the property representing the association
 706:      * in the parent entity cannot be traversed
 707:      * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns
 708:      * the saved entity
 709:      * @see \Cake\ORM\Table::save()
 710:      * @see \Cake\ORM\Association\BelongsToMany::replaceLinks()
 711:      */
 712:     public function saveAssociated(EntityInterface $entity, array $options = [])
 713:     {
 714:         $targetEntity = $entity->get($this->getProperty());
 715:         $strategy = $this->getSaveStrategy();
 716: 
 717:         $isEmpty = in_array($targetEntity, [null, [], '', false], true);
 718:         if ($isEmpty && $entity->isNew()) {
 719:             return $entity;
 720:         }
 721:         if ($isEmpty) {
 722:             $targetEntity = [];
 723:         }
 724: 
 725:         if ($strategy === self::SAVE_APPEND) {
 726:             return $this->_saveTarget($entity, $targetEntity, $options);
 727:         }
 728: 
 729:         if ($this->replaceLinks($entity, $targetEntity, $options)) {
 730:             return $entity;
 731:         }
 732: 
 733:         return false;
 734:     }
 735: 
 736:     /**
 737:      * Persists each of the entities into the target table and creates links between
 738:      * the parent entity and each one of the saved target entities.
 739:      *
 740:      * @param \Cake\Datasource\EntityInterface $parentEntity the source entity containing the target
 741:      * entities to be saved.
 742:      * @param array|\Traversable $entities list of entities to persist in target table and to
 743:      * link to the parent entity
 744:      * @param array $options list of options accepted by `Table::save()`
 745:      * @throws \InvalidArgumentException if the property representing the association
 746:      * in the parent entity cannot be traversed
 747:      * @return \Cake\Datasource\EntityInterface|bool The parent entity after all links have been
 748:      * created if no errors happened, false otherwise
 749:      */
 750:     protected function _saveTarget(EntityInterface $parentEntity, $entities, $options)
 751:     {
 752:         $joinAssociations = false;
 753:         if (!empty($options['associated'][$this->_junctionProperty]['associated'])) {
 754:             $joinAssociations = $options['associated'][$this->_junctionProperty]['associated'];
 755:         }
 756:         unset($options['associated'][$this->_junctionProperty]);
 757: 
 758:         if (!(is_array($entities) || $entities instanceof Traversable)) {
 759:             $name = $this->getProperty();
 760:             $message = sprintf('Could not save %s, it cannot be traversed', $name);
 761:             throw new InvalidArgumentException($message);
 762:         }
 763: 
 764:         $table = $this->getTarget();
 765:         $original = $entities;
 766:         $persisted = [];
 767: 
 768:         foreach ($entities as $k => $entity) {
 769:             if (!($entity instanceof EntityInterface)) {
 770:                 break;
 771:             }
 772: 
 773:             if (!empty($options['atomic'])) {
 774:                 $entity = clone $entity;
 775:             }
 776: 
 777:             $saved = $table->save($entity, $options);
 778:             if ($saved) {
 779:                 $entities[$k] = $entity;
 780:                 $persisted[] = $entity;
 781:                 continue;
 782:             }
 783: 
 784:             // Saving the new linked entity failed, copy errors back into the
 785:             // original entity if applicable and abort.
 786:             if (!empty($options['atomic'])) {
 787:                 $original[$k]->setErrors($entity->getErrors());
 788:             }
 789:             if (!$saved) {
 790:                 return false;
 791:             }
 792:         }
 793: 
 794:         $options['associated'] = $joinAssociations;
 795:         $success = $this->_saveLinks($parentEntity, $persisted, $options);
 796:         if (!$success && !empty($options['atomic'])) {
 797:             $parentEntity->set($this->getProperty(), $original);
 798: 
 799:             return false;
 800:         }
 801: 
 802:         $parentEntity->set($this->getProperty(), $entities);
 803: 
 804:         return $parentEntity;
 805:     }
 806: 
 807:     /**
 808:      * Creates links between the source entity and each of the passed target entities
 809:      *
 810:      * @param \Cake\Datasource\EntityInterface $sourceEntity the entity from source table in this
 811:      * association
 812:      * @param array $targetEntities list of entities to link to link to the source entity using the
 813:      * junction table
 814:      * @param array $options list of options accepted by `Table::save()`
 815:      * @return bool success
 816:      */
 817:     protected function _saveLinks(EntityInterface $sourceEntity, $targetEntities, $options)
 818:     {
 819:         $target = $this->getTarget();
 820:         $junction = $this->junction();
 821:         $entityClass = $junction->getEntityClass();
 822:         $belongsTo = $junction->getAssociation($target->getAlias());
 823:         $foreignKey = (array)$this->getForeignKey();
 824:         $assocForeignKey = (array)$belongsTo->getForeignKey();
 825:         $targetPrimaryKey = (array)$target->getPrimaryKey();
 826:         $bindingKey = (array)$this->getBindingKey();
 827:         $jointProperty = $this->_junctionProperty;
 828:         $junctionRegistryAlias = $junction->getRegistryAlias();
 829: 
 830:         foreach ($targetEntities as $e) {
 831:             $joint = $e->get($jointProperty);
 832:             if (!$joint || !($joint instanceof EntityInterface)) {
 833:                 $joint = new $entityClass([], ['markNew' => true, 'source' => $junctionRegistryAlias]);
 834:             }
 835:             $sourceKeys = array_combine($foreignKey, $sourceEntity->extract($bindingKey));
 836:             $targetKeys = array_combine($assocForeignKey, $e->extract($targetPrimaryKey));
 837: 
 838:             $changedKeys = (
 839:                 $sourceKeys !== $joint->extract($foreignKey) ||
 840:                 $targetKeys !== $joint->extract($assocForeignKey)
 841:             );
 842:             // Keys were changed, the junction table record _could_ be
 843:             // new. By clearing the primary key values, and marking the entity
 844:             // as new, we let save() sort out whether or not we have a new link
 845:             // or if we are updating an existing link.
 846:             if ($changedKeys) {
 847:                 $joint->isNew(true);
 848:                 $joint->unsetProperty($junction->getPrimaryKey())
 849:                     ->set(array_merge($sourceKeys, $targetKeys), ['guard' => false]);
 850:             }
 851:             $saved = $junction->save($joint, $options);
 852: 
 853:             if (!$saved && !empty($options['atomic'])) {
 854:                 return false;
 855:             }
 856: 
 857:             $e->set($jointProperty, $joint);
 858:             $e->setDirty($jointProperty, false);
 859:         }
 860: 
 861:         return true;
 862:     }
 863: 
 864:     /**
 865:      * Associates the source entity to each of the target entities provided by
 866:      * creating links in the junction table. Both the source entity and each of
 867:      * the target entities are assumed to be already persisted, if they are marked
 868:      * as new or their status is unknown then an exception will be thrown.
 869:      *
 870:      * When using this method, all entities in `$targetEntities` will be appended to
 871:      * the source entity's property corresponding to this association object.
 872:      *
 873:      * This method does not check link uniqueness.
 874:      *
 875:      * ### Example:
 876:      *
 877:      * ```
 878:      * $newTags = $tags->find('relevant')->toArray();
 879:      * $articles->getAssociation('tags')->link($article, $newTags);
 880:      * ```
 881:      *
 882:      * `$article->get('tags')` will contain all tags in `$newTags` after liking
 883:      *
 884:      * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
 885:      *   of this association
 886:      * @param array $targetEntities list of entities belonging to the `target` side
 887:      *   of this association
 888:      * @param array $options list of options to be passed to the internal `save` call
 889:      * @throws \InvalidArgumentException when any of the values in $targetEntities is
 890:      *   detected to not be already persisted
 891:      * @return bool true on success, false otherwise
 892:      */
 893:     public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
 894:     {
 895:         $this->_checkPersistenceStatus($sourceEntity, $targetEntities);
 896:         $property = $this->getProperty();
 897:         $links = $sourceEntity->get($property) ?: [];
 898:         $links = array_merge($links, $targetEntities);
 899:         $sourceEntity->set($property, $links);
 900: 
 901:         return $this->junction()->getConnection()->transactional(
 902:             function () use ($sourceEntity, $targetEntities, $options) {
 903:                 return $this->_saveLinks($sourceEntity, $targetEntities, $options);
 904:             }
 905:         );
 906:     }
 907: 
 908:     /**
 909:      * Removes all links between the passed source entity and each of the provided
 910:      * target entities. This method assumes that all passed objects are already persisted
 911:      * in the database and that each of them contain a primary key value.
 912:      *
 913:      * ### Options
 914:      *
 915:      * Additionally to the default options accepted by `Table::delete()`, the following
 916:      * keys are supported:
 917:      *
 918:      * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that
 919:      * are stored in `$sourceEntity` (default: true)
 920:      *
 921:      * By default this method will unset each of the entity objects stored inside the
 922:      * source entity.
 923:      *
 924:      * ### Example:
 925:      *
 926:      * ```
 927:      * $article->tags = [$tag1, $tag2, $tag3, $tag4];
 928:      * $tags = [$tag1, $tag2, $tag3];
 929:      * $articles->getAssociation('tags')->unlink($article, $tags);
 930:      * ```
 931:      *
 932:      * `$article->get('tags')` will contain only `[$tag4]` after deleting in the database
 933:      *
 934:      * @param \Cake\Datasource\EntityInterface $sourceEntity An entity persisted in the source table for
 935:      *   this association.
 936:      * @param array $targetEntities List of entities persisted in the target table for
 937:      *   this association.
 938:      * @param array|bool $options List of options to be passed to the internal `delete` call,
 939:      *   or a `boolean` as `cleanProperty` key shortcut.
 940:      * @throws \InvalidArgumentException If non persisted entities are passed or if
 941:      *   any of them is lacking a primary key value.
 942:      * @return bool Success
 943:      */
 944:     public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = [])
 945:     {
 946:         if (is_bool($options)) {
 947:             $options = [
 948:                 'cleanProperty' => $options
 949:             ];
 950:         } else {
 951:             $options += ['cleanProperty' => true];
 952:         }
 953: 
 954:         $this->_checkPersistenceStatus($sourceEntity, $targetEntities);
 955:         $property = $this->getProperty();
 956: 
 957:         $this->junction()->getConnection()->transactional(
 958:             function () use ($sourceEntity, $targetEntities, $options) {
 959:                 $links = $this->_collectJointEntities($sourceEntity, $targetEntities);
 960:                 foreach ($links as $entity) {
 961:                     $this->_junctionTable->delete($entity, $options);
 962:                 }
 963:             }
 964:         );
 965: 
 966:         $existing = $sourceEntity->get($property) ?: [];
 967:         if (!$options['cleanProperty'] || empty($existing)) {
 968:             return true;
 969:         }
 970: 
 971:         $storage = new SplObjectStorage();
 972:         foreach ($targetEntities as $e) {
 973:             $storage->attach($e);
 974:         }
 975: 
 976:         foreach ($existing as $k => $e) {
 977:             if ($storage->contains($e)) {
 978:                 unset($existing[$k]);
 979:             }
 980:         }
 981: 
 982:         $sourceEntity->set($property, array_values($existing));
 983:         $sourceEntity->setDirty($property, false);
 984: 
 985:         return true;
 986:     }
 987: 
 988:     /**
 989:      * {@inheritDoc}
 990:      */
 991:     public function setConditions($conditions)
 992:     {
 993:         parent::setConditions($conditions);
 994:         $this->_targetConditions = $this->_junctionConditions = null;
 995: 
 996:         return $this;
 997:     }
 998: 
 999:     /**
1000:      * Sets the current join table, either the name of the Table instance or the instance itself.
1001:      *
1002:      * @param string|\Cake\ORM\Table $through Name of the Table instance or the instance itself
1003:      * @return $this
1004:      */
1005:     public function setThrough($through)
1006:     {
1007:         $this->_through = $through;
1008: 
1009:         return $this;
1010:     }
1011: 
1012:     /**
1013:      * Gets the current join table, either the name of the Table instance or the instance itself.
1014:      *
1015:      * @return string|\Cake\ORM\Table
1016:      */
1017:     public function getThrough()
1018:     {
1019:         return $this->_through;
1020:     }
1021: 
1022:     /**
1023:      * Returns filtered conditions that reference the target table.
1024:      *
1025:      * Any string expressions, or expression objects will
1026:      * also be returned in this list.
1027:      *
1028:      * @return mixed Generally an array. If the conditions
1029:      *   are not an array, the association conditions will be
1030:      *   returned unmodified.
1031:      */
1032:     protected function targetConditions()
1033:     {
1034:         if ($this->_targetConditions !== null) {
1035:             return $this->_targetConditions;
1036:         }
1037:         $conditions = $this->getConditions();
1038:         if (!is_array($conditions)) {
1039:             return $conditions;
1040:         }
1041:         $matching = [];
1042:         $alias = $this->getAlias() . '.';
1043:         foreach ($conditions as $field => $value) {
1044:             if (is_string($field) && strpos($field, $alias) === 0) {
1045:                 $matching[$field] = $value;
1046:             } elseif (is_int($field) || $value instanceof ExpressionInterface) {
1047:                 $matching[$field] = $value;
1048:             }
1049:         }
1050: 
1051:         return $this->_targetConditions = $matching;
1052:     }
1053: 
1054:     /**
1055:      * Returns filtered conditions that specifically reference
1056:      * the junction table.
1057:      *
1058:      * @return array
1059:      */
1060:     protected function junctionConditions()
1061:     {
1062:         if ($this->_junctionConditions !== null) {
1063:             return $this->_junctionConditions;
1064:         }
1065:         $matching = [];
1066:         $conditions = $this->getConditions();
1067:         if (!is_array($conditions)) {
1068:             return $matching;
1069:         }
1070:         $alias = $this->_junctionAssociationName() . '.';
1071:         foreach ($conditions as $field => $value) {
1072:             $isString = is_string($field);
1073:             if ($isString && strpos($field, $alias) === 0) {
1074:                 $matching[$field] = $value;
1075:             }
1076:             // Assume that operators contain junction conditions.
1077:             // Trying to manage complex conditions could result in incorrect queries.
1078:             if ($isString && in_array(strtoupper($field), ['OR', 'NOT', 'AND', 'XOR'])) {
1079:                 $matching[$field] = $value;
1080:             }
1081:         }
1082: 
1083:         return $this->_junctionConditions = $matching;
1084:     }
1085: 
1086:     /**
1087:      * Proxies the finding operation to the target table's find method
1088:      * and modifies the query accordingly based of this association
1089:      * configuration.
1090:      *
1091:      * If your association includes conditions, the junction table will be
1092:      * included in the query's contained associations.
1093:      *
1094:      * @param string|array|null $type the type of query to perform, if an array is passed,
1095:      *   it will be interpreted as the `$options` parameter
1096:      * @param array $options The options to for the find
1097:      * @see \Cake\ORM\Table::find()
1098:      * @return \Cake\ORM\Query
1099:      */
1100:     public function find($type = null, array $options = [])
1101:     {
1102:         $type = $type ?: $this->getFinder();
1103:         list($type, $opts) = $this->_extractFinder($type);
1104:         $query = $this->getTarget()
1105:             ->find($type, $options + $opts)
1106:             ->where($this->targetConditions())
1107:             ->addDefaultTypes($this->getTarget());
1108: 
1109:         if (!$this->junctionConditions()) {
1110:             return $query;
1111:         }
1112: 
1113:         $belongsTo = $this->junction()->getAssociation($this->getTarget()->getAlias());
1114:         $conditions = $belongsTo->_joinCondition([
1115:             'foreignKey' => $this->getTargetForeignKey()
1116:         ]);
1117:         $conditions += $this->junctionConditions();
1118: 
1119:         return $this->_appendJunctionJoin($query, $conditions);
1120:     }
1121: 
1122:     /**
1123:      * Append a join to the junction table.
1124:      *
1125:      * @param \Cake\ORM\Query $query The query to append.
1126:      * @param string|array $conditions The query conditions to use.
1127:      * @return \Cake\ORM\Query The modified query.
1128:      */
1129:     protected function _appendJunctionJoin($query, $conditions)
1130:     {
1131:         $name = $this->_junctionAssociationName();
1132:         /** @var array $joins */
1133:         $joins = $query->clause('join');
1134:         $matching = [
1135:             $name => [
1136:                 'table' => $this->junction()->getTable(),
1137:                 'conditions' => $conditions,
1138:                 'type' => QueryInterface::JOIN_TYPE_INNER
1139:             ]
1140:         ];
1141: 
1142:         $assoc = $this->getTarget()->getAssociation($name);
1143:         $query
1144:             ->addDefaultTypes($assoc->getTarget())
1145:             ->join($matching + $joins, [], true);
1146: 
1147:         return $query;
1148:     }
1149: 
1150:     /**
1151:      * Replaces existing association links between the source entity and the target
1152:      * with the ones passed. This method does a smart cleanup, links that are already
1153:      * persisted and present in `$targetEntities` will not be deleted, new links will
1154:      * be created for the passed target entities that are not already in the database
1155:      * and the rest will be removed.
1156:      *
1157:      * For example, if an article is linked to tags 'cake' and 'framework' and you pass
1158:      * to this method an array containing the entities for tags 'cake', 'php' and 'awesome',
1159:      * only the link for cake will be kept in database, the link for 'framework' will be
1160:      * deleted and the links for 'php' and 'awesome' will be created.
1161:      *
1162:      * Existing links are not deleted and created again, they are either left untouched
1163:      * or updated so that potential extra information stored in the joint row is not
1164:      * lost. Updating the link row can be done by making sure the corresponding passed
1165:      * target entity contains the joint property with its primary key and any extra
1166:      * information to be stored.
1167:      *
1168:      * On success, the passed `$sourceEntity` will contain `$targetEntities` as value
1169:      * in the corresponding property for this association.
1170:      *
1171:      * This method assumes that links between both the source entity and each of the
1172:      * target entities are unique. That is, for any given row in the source table there
1173:      * can only be one link in the junction table pointing to any other given row in
1174:      * the target table.
1175:      *
1176:      * Additional options for new links to be saved can be passed in the third argument,
1177:      * check `Table::save()` for information on the accepted options.
1178:      *
1179:      * ### Example:
1180:      *
1181:      * ```
1182:      * $article->tags = [$tag1, $tag2, $tag3, $tag4];
1183:      * $articles->save($article);
1184:      * $tags = [$tag1, $tag3];
1185:      * $articles->getAssociation('tags')->replaceLinks($article, $tags);
1186:      * ```
1187:      *
1188:      * `$article->get('tags')` will contain only `[$tag1, $tag3]` at the end
1189:      *
1190:      * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
1191:      *   this association
1192:      * @param array $targetEntities list of entities from the target table to be linked
1193:      * @param array $options list of options to be passed to the internal `save`/`delete` calls
1194:      *   when persisting/updating new links, or deleting existing ones
1195:      * @throws \InvalidArgumentException if non persisted entities are passed or if
1196:      *   any of them is lacking a primary key value
1197:      * @return bool success
1198:      */
1199:     public function replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
1200:     {
1201:         $bindingKey = (array)$this->getBindingKey();
1202:         $primaryValue = $sourceEntity->extract($bindingKey);
1203: 
1204:         if (count(array_filter($primaryValue, 'strlen')) !== count($bindingKey)) {
1205:             $message = 'Could not find primary key value for source entity';
1206:             throw new InvalidArgumentException($message);
1207:         }
1208: 
1209:         return $this->junction()->getConnection()->transactional(
1210:             function () use ($sourceEntity, $targetEntities, $primaryValue, $options) {
1211:                 $foreignKey = array_map([$this->_junctionTable, 'aliasField'], (array)$this->getForeignKey());
1212:                 $hasMany = $this->getSource()->getAssociation($this->_junctionTable->getAlias());
1213:                 $existing = $hasMany->find('all')
1214:                     ->where(array_combine($foreignKey, $primaryValue));
1215: 
1216:                 $associationConditions = $this->getConditions();
1217:                 if ($associationConditions) {
1218:                     $existing->contain($this->getTarget()->getAlias());
1219:                     $existing->andWhere($associationConditions);
1220:                 }
1221: 
1222:                 $jointEntities = $this->_collectJointEntities($sourceEntity, $targetEntities);
1223:                 $inserts = $this->_diffLinks($existing, $jointEntities, $targetEntities, $options);
1224: 
1225:                 if ($inserts && !$this->_saveTarget($sourceEntity, $inserts, $options)) {
1226:                     return false;
1227:                 }
1228: 
1229:                 $property = $this->getProperty();
1230: 
1231:                 if (count($inserts)) {
1232:                     $inserted = array_combine(
1233:                         array_keys($inserts),
1234:                         (array)$sourceEntity->get($property)
1235:                     );
1236:                     $targetEntities = $inserted + $targetEntities;
1237:                 }
1238: 
1239:                 ksort($targetEntities);
1240:                 $sourceEntity->set($property, array_values($targetEntities));
1241:                 $sourceEntity->setDirty($property, false);
1242: 
1243:                 return true;
1244:             }
1245:         );
1246:     }
1247: 
1248:     /**
1249:      * Helper method used to delete the difference between the links passed in
1250:      * `$existing` and `$jointEntities`. This method will return the values from
1251:      * `$targetEntities` that were not deleted from calculating the difference.
1252:      *
1253:      * @param \Cake\ORM\Query $existing a query for getting existing links
1254:      * @param array $jointEntities link entities that should be persisted
1255:      * @param array $targetEntities entities in target table that are related to
1256:      * the `$jointEntities`
1257:      * @param array $options list of options accepted by `Table::delete()`
1258:      * @return array
1259:      */
1260:     protected function _diffLinks($existing, $jointEntities, $targetEntities, $options = [])
1261:     {
1262:         $junction = $this->junction();
1263:         $target = $this->getTarget();
1264:         $belongsTo = $junction->getAssociation($target->getAlias());
1265:         $foreignKey = (array)$this->getForeignKey();
1266:         $assocForeignKey = (array)$belongsTo->getForeignKey();
1267: 
1268:         $keys = array_merge($foreignKey, $assocForeignKey);
1269:         $deletes = $indexed = $present = [];
1270: 
1271:         foreach ($jointEntities as $i => $entity) {
1272:             $indexed[$i] = $entity->extract($keys);
1273:             $present[$i] = array_values($entity->extract($assocForeignKey));
1274:         }
1275: 
1276:         foreach ($existing as $result) {
1277:             $fields = $result->extract($keys);
1278:             $found = false;
1279:             foreach ($indexed as $i => $data) {
1280:                 if ($fields === $data) {
1281:                     unset($indexed[$i]);
1282:                     $found = true;
1283:                     break;
1284:                 }
1285:             }
1286: 
1287:             if (!$found) {
1288:                 $deletes[] = $result;
1289:             }
1290:         }
1291: 
1292:         $primary = (array)$target->getPrimaryKey();
1293:         $jointProperty = $this->_junctionProperty;
1294:         foreach ($targetEntities as $k => $entity) {
1295:             if (!($entity instanceof EntityInterface)) {
1296:                 continue;
1297:             }
1298:             $key = array_values($entity->extract($primary));
1299:             foreach ($present as $i => $data) {
1300:                 if ($key === $data && !$entity->get($jointProperty)) {
1301:                     unset($targetEntities[$k], $present[$i]);
1302:                     break;
1303:                 }
1304:             }
1305:         }
1306: 
1307:         if ($deletes) {
1308:             foreach ($deletes as $entity) {
1309:                 $junction->delete($entity, $options);
1310:             }
1311:         }
1312: 
1313:         return $targetEntities;
1314:     }
1315: 
1316:     /**
1317:      * Throws an exception should any of the passed entities is not persisted.
1318:      *
1319:      * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
1320:      *   of this association
1321:      * @param array $targetEntities list of entities belonging to the `target` side
1322:      *   of this association
1323:      * @return bool
1324:      * @throws \InvalidArgumentException
1325:      */
1326:     protected function _checkPersistenceStatus($sourceEntity, array $targetEntities)
1327:     {
1328:         if ($sourceEntity->isNew()) {
1329:             $error = 'Source entity needs to be persisted before links can be created or removed.';
1330:             throw new InvalidArgumentException($error);
1331:         }
1332: 
1333:         foreach ($targetEntities as $entity) {
1334:             if ($entity->isNew()) {
1335:                 $error = 'Cannot link entities that have not been persisted yet.';
1336:                 throw new InvalidArgumentException($error);
1337:             }
1338:         }
1339: 
1340:         return true;
1341:     }
1342: 
1343:     /**
1344:      * Returns the list of joint entities that exist between the source entity
1345:      * and each of the passed target entities
1346:      *
1347:      * @param \Cake\Datasource\EntityInterface $sourceEntity The row belonging to the source side
1348:      *   of this association.
1349:      * @param array $targetEntities The rows belonging to the target side of this
1350:      *   association.
1351:      * @throws \InvalidArgumentException if any of the entities is lacking a primary
1352:      *   key value
1353:      * @return array
1354:      */
1355:     protected function _collectJointEntities($sourceEntity, $targetEntities)
1356:     {
1357:         $target = $this->getTarget();
1358:         $source = $this->getSource();
1359:         $junction = $this->junction();
1360:         $jointProperty = $this->_junctionProperty;
1361:         $primary = (array)$target->getPrimaryKey();
1362: 
1363:         $result = [];
1364:         $missing = [];
1365: 
1366:         foreach ($targetEntities as $entity) {
1367:             if (!($entity instanceof EntityInterface)) {
1368:                 continue;
1369:             }
1370:             $joint = $entity->get($jointProperty);
1371: 
1372:             if (!$joint || !($joint instanceof EntityInterface)) {
1373:                 $missing[] = $entity->extract($primary);
1374:                 continue;
1375:             }
1376: 
1377:             $result[] = $joint;
1378:         }
1379: 
1380:         if (empty($missing)) {
1381:             return $result;
1382:         }
1383: 
1384:         $belongsTo = $junction->getAssociation($target->getAlias());
1385:         $hasMany = $source->getAssociation($junction->getAlias());
1386:         $foreignKey = (array)$this->getForeignKey();
1387:         $assocForeignKey = (array)$belongsTo->getForeignKey();
1388:         $sourceKey = $sourceEntity->extract((array)$source->getPrimaryKey());
1389: 
1390:         $unions = [];
1391:         foreach ($missing as $key) {
1392:             $unions[] = $hasMany->find('all')
1393:                 ->where(array_combine($foreignKey, $sourceKey))
1394:                 ->andWhere(array_combine($assocForeignKey, $key));
1395:         }
1396: 
1397:         $query = array_shift($unions);
1398:         foreach ($unions as $q) {
1399:             $query->union($q);
1400:         }
1401: 
1402:         return array_merge($result, $query->toArray());
1403:     }
1404: 
1405:     /**
1406:      * Returns the name of the association from the target table to the junction table,
1407:      * this name is used to generate alias in the query and to later on retrieve the
1408:      * results.
1409:      *
1410:      * @return string
1411:      */
1412:     protected function _junctionAssociationName()
1413:     {
1414:         if (!$this->_junctionAssociationName) {
1415:             $this->_junctionAssociationName = $this->getTarget()
1416:                 ->getAssociation($this->junction()->getAlias())
1417:                 ->getName();
1418:         }
1419: 
1420:         return $this->_junctionAssociationName;
1421:     }
1422: 
1423:     /**
1424:      * Sets the name of the junction table.
1425:      * If no arguments are passed the current configured name is returned. A default
1426:      * name based of the associated tables will be generated if none found.
1427:      *
1428:      * @param string|null $name The name of the junction table.
1429:      * @return string
1430:      */
1431:     protected function _junctionTableName($name = null)
1432:     {
1433:         if ($name === null) {
1434:             if (empty($this->_junctionTableName)) {
1435:                 $tablesNames = array_map('Cake\Utility\Inflector::underscore', [
1436:                     $this->getSource()->getTable(),
1437:                     $this->getTarget()->getTable()
1438:                 ]);
1439:                 sort($tablesNames);
1440:                 $this->_junctionTableName = implode('_', $tablesNames);
1441:             }
1442: 
1443:             return $this->_junctionTableName;
1444:         }
1445: 
1446:         return $this->_junctionTableName = $name;
1447:     }
1448: 
1449:     /**
1450:      * Parse extra options passed in the constructor.
1451:      *
1452:      * @param array $opts original list of options passed in constructor
1453:      * @return void
1454:      */
1455:     protected function _options(array $opts)
1456:     {
1457:         if (!empty($opts['targetForeignKey'])) {
1458:             $this->setTargetForeignKey($opts['targetForeignKey']);
1459:         }
1460:         if (!empty($opts['joinTable'])) {
1461:             $this->_junctionTableName($opts['joinTable']);
1462:         }
1463:         if (!empty($opts['through'])) {
1464:             $this->setThrough($opts['through']);
1465:         }
1466:         if (!empty($opts['saveStrategy'])) {
1467:             $this->setSaveStrategy($opts['saveStrategy']);
1468:         }
1469:         if (isset($opts['sort'])) {
1470:             $this->setSort($opts['sort']);
1471:         }
1472:     }
1473: }
1474: 
Follow @CakePHP
#IRC
OpenHub
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Logos & Trademarks
  • Community
  • Team
  • Issues (Github)
  • YouTube Channel
  • Get Involved
  • Bakery
  • Featured Resources
  • Newsletter
  • Certification
  • My CakePHP
  • CakeFest
  • Facebook
  • Twitter
  • Help & Support
  • Forum
  • Stack Overflow
  • IRC
  • Slack
  • Paid Support

Generated using CakePHP API Docs