Relationship.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <?php
  2. /**
  3. * @package ActiveRecord
  4. */
  5. namespace ActiveRecord;
  6. /**
  7. * Interface for a table relationship.
  8. *
  9. * @package ActiveRecord
  10. */
  11. interface InterfaceRelationship
  12. {
  13. public function __construct($options=array());
  14. public function build_association(Model $model, $attributes=array());
  15. public function create_association(Model $model, $attributes=array());
  16. }
  17. /**
  18. * Abstract class that all relationships must extend from.
  19. *
  20. * @package ActiveRecord
  21. * @see http://www.phpactiverecord.org/guides/associations
  22. */
  23. abstract class AbstractRelationship implements InterfaceRelationship
  24. {
  25. /**
  26. * Name to be used that will trigger call to the relationship.
  27. *
  28. * @var string
  29. */
  30. public $attribute_name;
  31. /**
  32. * Class name of the associated model.
  33. *
  34. * @var string
  35. */
  36. public $class_name;
  37. /**
  38. * Name of the foreign key.
  39. *
  40. * @var string
  41. */
  42. public $foreign_key = array();
  43. /**
  44. * Options of the relationship.
  45. *
  46. * @var array
  47. */
  48. protected $options = array();
  49. /**
  50. * Is the relationship single or multi.
  51. *
  52. * @var boolean
  53. */
  54. protected $poly_relationship = false;
  55. /**
  56. * List of valid options for relationships.
  57. *
  58. * @var array
  59. */
  60. static protected $valid_association_options = array('class_name', 'class', 'foreign_key', 'conditions', 'select', 'readonly', 'namespace');
  61. /**
  62. * Constructs a relationship.
  63. *
  64. * @param array $options Options for the relationship (see {@link valid_association_options})
  65. * @return mixed
  66. */
  67. public function __construct($options=array())
  68. {
  69. $this->attribute_name = $options[0];
  70. $this->options = $this->merge_association_options($options);
  71. $relationship = strtolower(denamespace(get_called_class()));
  72. if ($relationship === 'hasmany' || $relationship === 'hasandbelongstomany')
  73. $this->poly_relationship = true;
  74. if (isset($this->options['conditions']) && !is_array($this->options['conditions']))
  75. $this->options['conditions'] = array($this->options['conditions']);
  76. if (isset($this->options['class']))
  77. $this->set_class_name($this->options['class']);
  78. elseif (isset($this->options['class_name']))
  79. $this->set_class_name($this->options['class_name']);
  80. $this->attribute_name = strtolower(Inflector::instance()->variablize($this->attribute_name));
  81. if (!$this->foreign_key && isset($this->options['foreign_key']))
  82. $this->foreign_key = is_array($this->options['foreign_key']) ? $this->options['foreign_key'] : array($this->options['foreign_key']);
  83. }
  84. protected function get_table()
  85. {
  86. return Table::load($this->class_name);
  87. }
  88. /**
  89. * What is this relationship's cardinality?
  90. *
  91. * @return bool
  92. */
  93. public function is_poly()
  94. {
  95. return $this->poly_relationship;
  96. }
  97. /**
  98. * Eagerly loads relationships for $models.
  99. *
  100. * This method takes an array of models, collects PK or FK (whichever is needed for relationship), then queries
  101. * the related table by PK/FK and attaches the array of returned relationships to the appropriately named relationship on
  102. * $models.
  103. *
  104. * @param Table $table
  105. * @param $models array of model objects
  106. * @param $attributes array of attributes from $models
  107. * @param $includes array of eager load directives
  108. * @param $query_keys -> key(s) to be queried for on included/related table
  109. * @param $model_values_keys -> key(s)/value(s) to be used in query from model which is including
  110. * @return void
  111. */
  112. protected function query_and_attach_related_models_eagerly(Table $table, $models, $attributes, $includes=array(), $query_keys=array(), $model_values_keys=array())
  113. {
  114. $values = array();
  115. $options = $this->options;
  116. $inflector = Inflector::instance();
  117. $query_key = $query_keys[0];
  118. $model_values_key = $model_values_keys[0];
  119. foreach ($attributes as $column => $value)
  120. $values[] = $value[$inflector->variablize($model_values_key)];
  121. $values = array($values);
  122. $conditions = SQLBuilder::create_conditions_from_underscored_string($table->conn,$query_key,$values);
  123. if (isset($options['conditions']) && strlen($options['conditions'][0]) > 1)
  124. Utils::add_condition($options['conditions'], $conditions);
  125. else
  126. $options['conditions'] = $conditions;
  127. if (!empty($includes))
  128. $options['include'] = $includes;
  129. if (!empty($options['through'])) {
  130. // save old keys as we will be reseting them below for inner join convenience
  131. $pk = $this->primary_key;
  132. $fk = $this->foreign_key;
  133. $this->set_keys($this->get_table()->class->getName(), true);
  134. if (!isset($options['class_name'])) {
  135. $class = classify($options['through'], true);
  136. if (isset($this->options['namespace']) && !class_exists($class))
  137. $class = $this->options['namespace'].'\\'.$class;
  138. $through_table = $class::table();
  139. } else {
  140. $class = $options['class_name'];
  141. $relation = $class::table()->get_relationship($options['through']);
  142. $through_table = $relation->get_table();
  143. }
  144. $options['joins'] = $this->construct_inner_join_sql($through_table, true);
  145. $query_key = $this->primary_key[0];
  146. // reset keys
  147. $this->primary_key = $pk;
  148. $this->foreign_key = $fk;
  149. }
  150. $options = $this->unset_non_finder_options($options);
  151. $class = $this->class_name;
  152. $related_models = $class::find('all', $options);
  153. $used_models = array();
  154. $model_values_key = $inflector->variablize($model_values_key);
  155. $query_key = $inflector->variablize($query_key);
  156. foreach ($models as $model)
  157. {
  158. $matches = 0;
  159. $key_to_match = $model->$model_values_key;
  160. foreach ($related_models as $related)
  161. {
  162. if ($related->$query_key == $key_to_match)
  163. {
  164. $hash = spl_object_hash($related);
  165. if (in_array($hash, $used_models))
  166. $model->set_relationship_from_eager_load(clone($related), $this->attribute_name);
  167. else
  168. $model->set_relationship_from_eager_load($related, $this->attribute_name);
  169. $used_models[] = $hash;
  170. $matches++;
  171. }
  172. }
  173. if (0 === $matches)
  174. $model->set_relationship_from_eager_load(null, $this->attribute_name);
  175. }
  176. }
  177. /**
  178. * Creates a new instance of specified {@link Model} with the attributes pre-loaded.
  179. *
  180. * @param Model $model The model which holds this association
  181. * @param array $attributes Hash containing attributes to initialize the model with
  182. * @return Model
  183. */
  184. public function build_association(Model $model, $attributes=array())
  185. {
  186. $class_name = $this->class_name;
  187. return new $class_name($attributes);
  188. }
  189. /**
  190. * Creates a new instance of {@link Model} and invokes save.
  191. *
  192. * @param Model $model The model which holds this association
  193. * @param array $attributes Hash containing attributes to initialize the model with
  194. * @return Model
  195. */
  196. public function create_association(Model $model, $attributes=array())
  197. {
  198. $class_name = $this->class_name;
  199. $new_record = $class_name::create($attributes);
  200. return $this->append_record_to_associate($model, $new_record);
  201. }
  202. protected function append_record_to_associate(Model $associate, Model $record)
  203. {
  204. $association =& $associate->{$this->attribute_name};
  205. if ($this->poly_relationship)
  206. $association[] = $record;
  207. else
  208. $association = $record;
  209. return $record;
  210. }
  211. protected function merge_association_options($options)
  212. {
  213. $available_options = array_merge(self::$valid_association_options,static::$valid_association_options);
  214. $valid_options = array_intersect_key(array_flip($available_options),$options);
  215. foreach ($valid_options as $option => $v)
  216. $valid_options[$option] = $options[$option];
  217. return $valid_options;
  218. }
  219. protected function unset_non_finder_options($options)
  220. {
  221. foreach (array_keys($options) as $option)
  222. {
  223. if (!in_array($option, Model::$VALID_OPTIONS))
  224. unset($options[$option]);
  225. }
  226. return $options;
  227. }
  228. /**
  229. * Infers the $this->class_name based on $this->attribute_name.
  230. *
  231. * Will try to guess the appropriate class by singularizing and uppercasing $this->attribute_name.
  232. *
  233. * @return void
  234. * @see attribute_name
  235. */
  236. protected function set_inferred_class_name()
  237. {
  238. $singularize = ($this instanceOf HasMany ? true : false);
  239. $this->set_class_name(classify($this->attribute_name, $singularize));
  240. }
  241. protected function set_class_name($class_name)
  242. {
  243. try {
  244. $reflection = Reflections::instance()->add($class_name)->get($class_name);
  245. } catch (\ReflectionException $e) {
  246. if (isset($this->options['namespace'])) {
  247. $class_name = $this->options['namespace'].'\\'.$class_name;
  248. $reflection = Reflections::instance()->add($class_name)->get($class_name);
  249. } else {
  250. throw $e;
  251. }
  252. }
  253. if (!$reflection->isSubClassOf('ActiveRecord\\Model'))
  254. throw new RelationshipException("'$class_name' must extend from ActiveRecord\\Model");
  255. $this->class_name = $class_name;
  256. }
  257. protected function create_conditions_from_keys(Model $model, $condition_keys=array(), $value_keys=array())
  258. {
  259. $condition_string = implode('_and_', $condition_keys);
  260. $condition_values = array_values($model->get_values_for($value_keys));
  261. // return null if all the foreign key values are null so that we don't try to do a query like "id is null"
  262. if (all(null,$condition_values))
  263. return null;
  264. $conditions = SQLBuilder::create_conditions_from_underscored_string(Table::load(get_class($model))->conn,$condition_string,$condition_values);
  265. # DO NOT CHANGE THE NEXT TWO LINES. add_condition operates on a reference and will screw options array up
  266. if (isset($this->options['conditions']))
  267. $options_conditions = $this->options['conditions'];
  268. else
  269. $options_conditions = array();
  270. return Utils::add_condition($options_conditions, $conditions);
  271. }
  272. /**
  273. * Creates INNER JOIN SQL for associations.
  274. *
  275. * @param Table $from_table the table used for the FROM SQL statement
  276. * @param bool $using_through is this a THROUGH relationship?
  277. * @param string $alias a table alias for when a table is being joined twice
  278. * @return string SQL INNER JOIN fragment
  279. */
  280. public function construct_inner_join_sql(Table $from_table, $using_through=false, $alias=null)
  281. {
  282. if ($using_through)
  283. {
  284. $join_table = $from_table;
  285. $join_table_name = $from_table->get_fully_qualified_table_name();
  286. $from_table_name = Table::load($this->class_name)->get_fully_qualified_table_name();
  287. }
  288. else
  289. {
  290. $join_table = Table::load($this->class_name);
  291. $join_table_name = $join_table->get_fully_qualified_table_name();
  292. $from_table_name = $from_table->get_fully_qualified_table_name();
  293. }
  294. // need to flip the logic when the key is on the other table
  295. if ($this instanceof HasMany || $this instanceof HasOne)
  296. {
  297. $this->set_keys($from_table->class->getName());
  298. if ($using_through)
  299. {
  300. $foreign_key = $this->primary_key[0];
  301. $join_primary_key = $this->foreign_key[0];
  302. }
  303. else
  304. {
  305. $join_primary_key = $this->foreign_key[0];
  306. $foreign_key = $this->primary_key[0];
  307. }
  308. }
  309. else
  310. {
  311. $foreign_key = $this->foreign_key[0];
  312. $join_primary_key = $this->primary_key[0];
  313. }
  314. if (!is_null($alias))
  315. {
  316. $aliased_join_table_name = $alias = $this->get_table()->conn->quote_name($alias);
  317. $alias .= ' ';
  318. }
  319. else
  320. $aliased_join_table_name = $join_table_name;
  321. return "INNER JOIN $join_table_name {$alias}ON($from_table_name.$foreign_key = $aliased_join_table_name.$join_primary_key)";
  322. }
  323. /**
  324. * This will load the related model data.
  325. *
  326. * @param Model $model The model this relationship belongs to
  327. */
  328. abstract function load(Model $model);
  329. };
  330. /**
  331. * One-to-many relationship.
  332. *
  333. * <code>
  334. * # Table: people
  335. * # Primary key: id
  336. * # Foreign key: school_id
  337. * class Person extends ActiveRecord\Model {}
  338. *
  339. * # Table: schools
  340. * # Primary key: id
  341. * class School extends ActiveRecord\Model {
  342. * static $has_many = array(
  343. * array('people')
  344. * );
  345. * });
  346. * </code>
  347. *
  348. * Example using options:
  349. *
  350. * <code>
  351. * class Payment extends ActiveRecord\Model {
  352. * static $belongs_to = array(
  353. * array('person'),
  354. * array('order')
  355. * );
  356. * }
  357. *
  358. * class Order extends ActiveRecord\Model {
  359. * static $has_many = array(
  360. * array('people',
  361. * 'through' => 'payments',
  362. * 'select' => 'people.*, payments.amount',
  363. * 'conditions' => 'payments.amount < 200')
  364. * );
  365. * }
  366. * </code>
  367. *
  368. * @package ActiveRecord
  369. * @see http://www.phpactiverecord.org/guides/associations
  370. * @see valid_association_options
  371. */
  372. class HasMany extends AbstractRelationship
  373. {
  374. /**
  375. * Valid options to use for a {@link HasMany} relationship.
  376. *
  377. * <ul>
  378. * <li><b>limit/offset:</b> limit the number of records</li>
  379. * <li><b>primary_key:</b> name of the primary_key of the association (defaults to "id")</li>
  380. * <li><b>group:</b> GROUP BY clause</li>
  381. * <li><b>order:</b> ORDER BY clause</li>
  382. * <li><b>through:</b> name of a model</li>
  383. * </ul>
  384. *
  385. * @var array
  386. */
  387. static protected $valid_association_options = array('primary_key', 'order', 'group', 'having', 'limit', 'offset', 'through', 'source');
  388. protected $primary_key;
  389. private $has_one = false;
  390. private $through;
  391. /**
  392. * Constructs a {@link HasMany} relationship.
  393. *
  394. * @param array $options Options for the association
  395. * @return HasMany
  396. */
  397. public function __construct($options=array())
  398. {
  399. parent::__construct($options);
  400. if (isset($this->options['through']))
  401. {
  402. $this->through = $this->options['through'];
  403. if (isset($this->options['source']))
  404. $this->set_class_name($this->options['source']);
  405. }
  406. if (!$this->primary_key && isset($this->options['primary_key']))
  407. $this->primary_key = is_array($this->options['primary_key']) ? $this->options['primary_key'] : array($this->options['primary_key']);
  408. if (!$this->class_name)
  409. $this->set_inferred_class_name();
  410. }
  411. protected function set_keys($model_class_name, $override=false)
  412. {
  413. //infer from class_name
  414. if (!$this->foreign_key || $override)
  415. $this->foreign_key = array(Inflector::instance()->keyify($model_class_name));
  416. if (!$this->primary_key || $override)
  417. $this->primary_key = Table::load($model_class_name)->pk;
  418. }
  419. public function load(Model $model)
  420. {
  421. $class_name = $this->class_name;
  422. $this->set_keys(get_class($model));
  423. // since through relationships depend on other relationships we can't do
  424. // this initiailization in the constructor since the other relationship
  425. // may not have been created yet and we only want this to run once
  426. if (!isset($this->initialized))
  427. {
  428. if ($this->through)
  429. {
  430. // verify through is a belongs_to or has_many for access of keys
  431. if (!($through_relationship = $this->get_table()->get_relationship($this->through)))
  432. throw new HasManyThroughAssociationException("Could not find the association $this->through in model " . get_class($model));
  433. if (!($through_relationship instanceof HasMany) && !($through_relationship instanceof BelongsTo))
  434. throw new HasManyThroughAssociationException('has_many through can only use a belongs_to or has_many association');
  435. // save old keys as we will be reseting them below for inner join convenience
  436. $pk = $this->primary_key;
  437. $fk = $this->foreign_key;
  438. $this->set_keys($this->get_table()->class->getName(), true);
  439. $class = $this->class_name;
  440. $relation = $class::table()->get_relationship($this->through);
  441. $through_table = $relation->get_table();
  442. $this->options['joins'] = $this->construct_inner_join_sql($through_table, true);
  443. // reset keys
  444. $this->primary_key = $pk;
  445. $this->foreign_key = $fk;
  446. }
  447. $this->initialized = true;
  448. }
  449. if (!($conditions = $this->create_conditions_from_keys($model, $this->foreign_key, $this->primary_key)))
  450. return null;
  451. $options = $this->unset_non_finder_options($this->options);
  452. $options['conditions'] = $conditions;
  453. return $class_name::find($this->poly_relationship ? 'all' : 'first',$options);
  454. }
  455. private function inject_foreign_key_for_new_association(Model $model, &$attributes)
  456. {
  457. $this->set_keys($model);
  458. $primary_key = Inflector::instance()->variablize($this->foreign_key[0]);
  459. if (!isset($attributes[$primary_key]))
  460. $attributes[$primary_key] = $model->id;
  461. return $attributes;
  462. }
  463. public function build_association(Model $model, $attributes=array())
  464. {
  465. $attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
  466. return parent::build_association($model, $attributes);
  467. }
  468. public function create_association(Model $model, $attributes=array())
  469. {
  470. $attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
  471. return parent::create_association($model, $attributes);
  472. }
  473. public function load_eagerly($models=array(), $attributes=array(), $includes, Table $table)
  474. {
  475. $this->set_keys($table->class->name);
  476. $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes,$this->foreign_key, $table->pk);
  477. }
  478. };
  479. /**
  480. * One-to-one relationship.
  481. *
  482. * <code>
  483. * # Table name: states
  484. * # Primary key: id
  485. * class State extends ActiveRecord\Model {}
  486. *
  487. * # Table name: people
  488. * # Foreign key: state_id
  489. * class Person extends ActiveRecord\Model {
  490. * static $has_one = array(array('state'));
  491. * }
  492. * </code>
  493. *
  494. * @package ActiveRecord
  495. * @see http://www.phpactiverecord.org/guides/associations
  496. */
  497. class HasOne extends HasMany
  498. {
  499. };
  500. /**
  501. * @todo implement me
  502. * @package ActiveRecord
  503. * @see http://www.phpactiverecord.org/guides/associations
  504. */
  505. class HasAndBelongsToMany extends AbstractRelationship
  506. {
  507. public function __construct($options=array())
  508. {
  509. /* options =>
  510. * join_table - name of the join table if not in lexical order
  511. * foreign_key -
  512. * association_foreign_key - default is {assoc_class}_id
  513. * uniq - if true duplicate assoc objects will be ignored
  514. * validate
  515. */
  516. }
  517. public function load(Model $model)
  518. {
  519. }
  520. };
  521. /**
  522. * Belongs to relationship.
  523. *
  524. * <code>
  525. * class School extends ActiveRecord\Model {}
  526. *
  527. * class Person extends ActiveRecord\Model {
  528. * static $belongs_to = array(
  529. * array('school')
  530. * );
  531. * }
  532. * </code>
  533. *
  534. * Example using options:
  535. *
  536. * <code>
  537. * class School extends ActiveRecord\Model {}
  538. *
  539. * class Person extends ActiveRecord\Model {
  540. * static $belongs_to = array(
  541. * array('school', 'primary_key' => 'school_id')
  542. * );
  543. * }
  544. * </code>
  545. *
  546. * @package ActiveRecord
  547. * @see valid_association_options
  548. * @see http://www.phpactiverecord.org/guides/associations
  549. */
  550. class BelongsTo extends AbstractRelationship
  551. {
  552. public function __construct($options=array())
  553. {
  554. parent::__construct($options);
  555. if (!$this->class_name)
  556. $this->set_inferred_class_name();
  557. //infer from class_name
  558. if (!$this->foreign_key)
  559. $this->foreign_key = array(Inflector::instance()->keyify($this->class_name));
  560. }
  561. public function __get($name)
  562. {
  563. if($name === 'primary_key' && !isset($this->primary_key)) {
  564. $this->primary_key = array(Table::load($this->class_name)->pk[0]);
  565. }
  566. return $this->$name;
  567. }
  568. public function load(Model $model)
  569. {
  570. $keys = array();
  571. $inflector = Inflector::instance();
  572. foreach ($this->foreign_key as $key)
  573. $keys[] = $inflector->variablize($key);
  574. if (!($conditions = $this->create_conditions_from_keys($model, $this->primary_key, $keys)))
  575. return null;
  576. $options = $this->unset_non_finder_options($this->options);
  577. $options['conditions'] = $conditions;
  578. $class = $this->class_name;
  579. return $class::first($options);
  580. }
  581. public function load_eagerly($models=array(), $attributes, $includes, Table $table)
  582. {
  583. $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes, $this->primary_key,$this->foreign_key);
  584. }
  585. };
  586. ?>