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.8 Red Velvet API

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