PageRenderTime 31ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/core/phpactiverecord/Relationship.php

https://github.com/rosianesrocha/nanico
PHP | 637 lines | 310 code | 91 blank | 236 comment | 41 complexity | 04b4c915a05be2a59ebce2a825780ac2 MD5 | raw file
  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');
  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 = array();
  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. $options['conditions'] = SQLBuilder::create_conditions_from_underscored_string($table->conn,$query_key,$values);
  123. if (!empty($includes))
  124. $options['include'] = $includes;
  125. $class = $this->class_name;
  126. $related_models = $class::find('all', $options);
  127. $used_models = array();
  128. $model_values_key = $inflector->variablize($model_values_key);
  129. $query_key = $inflector->variablize($query_key);
  130. foreach ($models as $model)
  131. {
  132. $matches = 0;
  133. $key_to_match = $model->$model_values_key;
  134. foreach ($related_models as $related)
  135. {
  136. if ($related->$query_key == $key_to_match)
  137. {
  138. $hash = spl_object_hash($related);
  139. if (in_array($hash, $used_models))
  140. $model->set_relationship_from_eager_load(clone($related), $this->attribute_name);
  141. else
  142. $model->set_relationship_from_eager_load($related, $this->attribute_name);
  143. $used_models[] = $hash;
  144. $matches++;
  145. }
  146. }
  147. if (0 === $matches)
  148. $model->set_relationship_from_eager_load(null, $this->attribute_name);
  149. }
  150. }
  151. /**
  152. * Creates a new instance of specified {@link Model} with the attributes pre-loaded.
  153. *
  154. * @param Model $model The model which holds this association
  155. * @param array $attributes Hash containing attributes to initialize the model with
  156. * @return Model
  157. */
  158. public function build_association(Model $model, $attributes=array())
  159. {
  160. $class_name = $this->class_name;
  161. return new $class_name($attributes);
  162. }
  163. /**
  164. * Creates a new instance of {@link Model} and invokes save.
  165. *
  166. * @param Model $model The model which holds this association
  167. * @param array $attributes Hash containing attributes to initialize the model with
  168. * @return Model
  169. */
  170. public function create_association(Model $model, $attributes=array())
  171. {
  172. $class_name = $this->class_name;
  173. $new_record = $class_name::create($attributes);
  174. return $this->append_record_to_associate($model, $new_record);
  175. }
  176. protected function append_record_to_associate(Model $associate, Model $record)
  177. {
  178. $association =& $associate->{$this->attribute_name};
  179. if ($this->poly_relationship)
  180. $association[] = $record;
  181. else
  182. $association = $record;
  183. return $record;
  184. }
  185. protected function merge_association_options($options)
  186. {
  187. $available_options = array_merge(self::$valid_association_options,static::$valid_association_options);
  188. $valid_options = array_intersect_key(array_flip($available_options),$options);
  189. foreach ($valid_options as $option => $v)
  190. $valid_options[$option] = $options[$option];
  191. return $valid_options;
  192. }
  193. protected function unset_non_finder_options($options)
  194. {
  195. foreach (array_keys($options) as $option)
  196. {
  197. if (!in_array($option, Model::$VALID_OPTIONS))
  198. unset($options[$option]);
  199. }
  200. return $options;
  201. }
  202. protected function keyify($class_name)
  203. {
  204. return strtolower(classify(denamespace($class_name))). '_id';
  205. }
  206. /**
  207. * Infers the $this->class_name based on $this->attribute_name.
  208. *
  209. * Will try to guess the appropriate class by singularizing and uppercasing $this->attribute_name.
  210. *
  211. * @return void
  212. * @see attribute_name
  213. */
  214. protected function set_inferred_class_name()
  215. {
  216. $this->set_class_name(classify($this->attribute_name, true));
  217. }
  218. protected function set_class_name($class_name)
  219. {
  220. $reflection = Reflections::instance()->add($class_name)->get($class_name);
  221. if (!$reflection->isSubClassOf('ActiveRecord\\Model'))
  222. throw new RelationshipException("'$class_name' must extend from ActiveRecord\\Model");
  223. $this->class_name = $class_name;
  224. }
  225. protected function create_conditions_from_keys(Model $model, $condition_keys=array(), $value_keys=array())
  226. {
  227. $condition_string = implode('_and_', $condition_keys);
  228. $condition_values = array_values($model->get_values_for($value_keys));
  229. // return null if all the foreign key values are null so that we don't try to do a query like "id is null"
  230. if (all(null,$condition_values))
  231. return null;
  232. $conditions = SQLBuilder::create_conditions_from_underscored_string(Table::load(get_class($model))->conn,$condition_string,$condition_values);
  233. # DO NOT CHANGE THE NEXT TWO LINES. add_condition operates on a reference and will screw options array up
  234. if (isset($this->options['conditions']))
  235. $options_conditions = $this->options['conditions'];
  236. else
  237. $options_conditions = array();
  238. return Utils::add_condition($options_conditions, $conditions);
  239. }
  240. /**
  241. * Creates INNER JOIN SQL for associations.
  242. *
  243. * @param Table $from_table the table used for the FROM SQL statement
  244. * @param bool $using_through is this a THROUGH relationship?
  245. * @param string $alias a table alias for when a table is being joined twice
  246. * @return string SQL INNER JOIN fragment
  247. */
  248. public function construct_inner_join_sql(Table $from_table, $using_through=false, $alias=null)
  249. {
  250. if ($using_through)
  251. {
  252. $join_table = $from_table;
  253. $join_table_name = $from_table->get_fully_qualified_table_name();
  254. $from_table_name = Table::load($this->class_name)->get_fully_qualified_table_name();
  255. }
  256. else
  257. {
  258. $join_table = Table::load($this->class_name);
  259. $join_table_name = $join_table->get_fully_qualified_table_name();
  260. $from_table_name = $from_table->get_fully_qualified_table_name();
  261. }
  262. // need to flip the logic when the key is on the other table
  263. if ($this instanceof HasMany || $this instanceof HasOne)
  264. {
  265. $this->set_keys($from_table->class->getName());
  266. if ($using_through)
  267. {
  268. $foreign_key = $this->primary_key[0];
  269. $join_primary_key = $this->foreign_key[0];
  270. }
  271. else
  272. {
  273. $join_primary_key = $this->foreign_key[0];
  274. $foreign_key = $this->primary_key[0];
  275. }
  276. }
  277. else
  278. {
  279. $foreign_key = $this->foreign_key[0];
  280. $join_primary_key = $this->primary_key[0];
  281. }
  282. if (!is_null($alias))
  283. {
  284. $aliased_join_table_name = $alias = $this->get_table()->conn->quote_name($alias);
  285. $alias .= ' ';
  286. }
  287. else
  288. $aliased_join_table_name = $join_table_name;
  289. return "INNER JOIN $join_table_name {$alias}ON($from_table_name.$foreign_key = $aliased_join_table_name.$join_primary_key)";
  290. }
  291. /**
  292. * This will load the related model data.
  293. *
  294. * @param Model $model The model this relationship belongs to
  295. */
  296. abstract function load(Model $model);
  297. };
  298. /**
  299. * One-to-many relationship.
  300. *
  301. * <code>
  302. * # Table: people
  303. * # Primary key: id
  304. * # Foreign key: school_id
  305. * class Person extends ActiveRecord\Model {}
  306. *
  307. * # Table: schools
  308. * # Primary key: id
  309. * class School extends ActiveRecord\Model {
  310. * static $has_many = array(
  311. * array('people')
  312. * );
  313. * });
  314. * </code>
  315. *
  316. * Example using options:
  317. *
  318. * <code>
  319. * class Payment extends ActiveRecord\Model {
  320. * static $belongs_to = array(
  321. * array('person'),
  322. * array('order')
  323. * );
  324. * }
  325. *
  326. * class Order extends ActiveRecord\Model {
  327. * static $has_many = array(
  328. * array('people',
  329. * 'through' => 'payments',
  330. * 'select' => 'people.*, payments.amount',
  331. * 'conditions' => 'payments.amount < 200')
  332. * );
  333. * }
  334. * </code>
  335. *
  336. * @package ActiveRecord
  337. * @see http://www.phpactiverecord.org/guides/associations
  338. * @see valid_association_options
  339. */
  340. class HasMany extends AbstractRelationship
  341. {
  342. /**
  343. * Valid options to use for a {@link HasMany} relationship.
  344. *
  345. * <ul>
  346. * <li><b>limit/offset:</b> limit the number of records</li>
  347. * <li><b>primary_key:</b> name of the primary_key of the association (defaults to "id")</li>
  348. * <li><b>group:</b> GROUP BY clause</li>
  349. * <li><b>order:</b> ORDER BY clause</li>
  350. * <li><b>through:</b> name of a model</li>
  351. * </ul>
  352. *
  353. * @var array
  354. */
  355. static protected $valid_association_options = array('primary_key', 'order', 'group', 'having', 'limit', 'offset', 'through', 'source');
  356. protected $primary_key;
  357. private $has_one = false;
  358. private $through;
  359. /**
  360. * Constructs a {@link HasMany} relationship.
  361. *
  362. * @param array $options Options for the association
  363. * @return HasMany
  364. */
  365. public function __construct($options=array())
  366. {
  367. parent::__construct($options);
  368. if (isset($this->options['through']))
  369. {
  370. $this->through = $this->options['through'];
  371. if (isset($this->options['source']))
  372. $this->set_class_name($this->options['source']);
  373. }
  374. if (!$this->primary_key && isset($this->options['primary_key']))
  375. $this->primary_key = is_array($this->options['primary_key']) ? $this->options['primary_key'] : array($this->options['primary_key']);
  376. if (!$this->class_name)
  377. $this->set_inferred_class_name();
  378. }
  379. protected function set_keys($model_class_name, $override=false)
  380. {
  381. //infer from class_name
  382. if (!$this->foreign_key || $override)
  383. $this->foreign_key = array($this->keyify($model_class_name));
  384. if (!$this->primary_key || $override)
  385. $this->primary_key = Table::load($model_class_name)->pk;
  386. }
  387. public function load(Model $model)
  388. {
  389. $class_name = $this->class_name;
  390. $this->set_keys(get_class($model));
  391. // since through relationships depend on other relationships we can't do
  392. // this initiailization in the constructor since the other relationship
  393. // may not have been created yet and we only want this to run once
  394. if (!isset($this->initialized))
  395. {
  396. if ($this->through)
  397. {
  398. // verify through is a belongs_to or has_many for access of keys
  399. if (!($through_relationship = $this->get_table()->get_relationship($this->through)))
  400. throw new HasManyThroughAssociationException("Could not find the association $this->through in model " . get_class($model));
  401. if (!($through_relationship instanceof HasMany) && !($through_relationship instanceof BelongsTo))
  402. throw new HasManyThroughAssociationException('has_many through can only use a belongs_to or has_many association');
  403. // save old keys as we will be reseting them below for inner join convenience
  404. $pk = $this->primary_key;
  405. $fk = $this->foreign_key;
  406. $this->set_keys($this->get_table()->class->getName(), true);
  407. $through_table = Table::load(classify($this->through, true));
  408. $this->options['joins'] = $this->construct_inner_join_sql($through_table, true);
  409. // reset keys
  410. $this->primary_key = $pk;
  411. $this->foreign_key = $fk;
  412. }
  413. $this->initialized = true;
  414. }
  415. if (!($conditions = $this->create_conditions_from_keys($model, $this->foreign_key, $this->primary_key)))
  416. return null;
  417. $options = $this->unset_non_finder_options($this->options);
  418. $options['conditions'] = $conditions;
  419. return $class_name::find($this->poly_relationship ? 'all' : 'first',$options);
  420. }
  421. private function inject_foreign_key_for_new_association(Model $model, &$attributes)
  422. {
  423. $this->set_keys($model);
  424. $primary_key = Inflector::instance()->variablize($this->foreign_key[0]);
  425. if (!isset($attributes[$primary_key]))
  426. $attributes[$primary_key] = $model->id;
  427. return $attributes;
  428. }
  429. public function build_association(Model $model, $attributes=array())
  430. {
  431. $attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
  432. return parent::build_association($model, $attributes);
  433. }
  434. public function create_association(Model $model, $attributes=array())
  435. {
  436. $attributes = $this->inject_foreign_key_for_new_association($model, $attributes);
  437. return parent::create_association($model, $attributes);
  438. }
  439. public function load_eagerly($models=array(), $attributes=array(), $includes, Table $table)
  440. {
  441. $this->set_keys($table->class->name);
  442. $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes,$this->foreign_key, $table->pk);
  443. }
  444. };
  445. /**
  446. * One-to-one relationship.
  447. *
  448. * <code>
  449. * # Table name: states
  450. * # Primary key: id
  451. * class State extends ActiveRecord\Model {}
  452. *
  453. * # Table name: people
  454. * # Foreign key: state_id
  455. * class Person extends ActiveRecord\Model {
  456. * static $has_one = array(array('state'));
  457. * }
  458. * </code>
  459. *
  460. * @package ActiveRecord
  461. * @see http://www.phpactiverecord.org/guides/associations
  462. */
  463. class HasOne extends HasMany
  464. {
  465. };
  466. /**
  467. * @todo implement me
  468. * @package ActiveRecord
  469. * @see http://www.phpactiverecord.org/guides/associations
  470. */
  471. class HasAndBelongsToMany extends AbstractRelationship
  472. {
  473. public function __construct($options=array())
  474. {
  475. /* options =>
  476. * join_table - name of the join table if not in lexical order
  477. * foreign_key -
  478. * association_foreign_key - default is {assoc_class}_id
  479. * uniq - if true duplicate assoc objects will be ignored
  480. * validate
  481. */
  482. }
  483. public function load(Model $model)
  484. {
  485. }
  486. };
  487. /**
  488. * Belongs to relationship.
  489. *
  490. * <code>
  491. * class School extends ActiveRecord\Model {}
  492. *
  493. * class Person extends ActiveRecord\Model {
  494. * static $belongs_to = array(
  495. * array('school')
  496. * );
  497. * }
  498. * </code>
  499. *
  500. * Example using options:
  501. *
  502. * <code>
  503. * class School extends ActiveRecord\Model {}
  504. *
  505. * class Person extends ActiveRecord\Model {
  506. * static $belongs_to = array(
  507. * array('school', 'primary_key' => 'school_id')
  508. * );
  509. * }
  510. * </code>
  511. *
  512. * @package ActiveRecord
  513. * @see valid_association_options
  514. * @see http://www.phpactiverecord.org/guides/associations
  515. */
  516. class BelongsTo extends AbstractRelationship
  517. {
  518. public function __construct($options=array())
  519. {
  520. parent::__construct($options);
  521. if (!$this->class_name)
  522. $this->set_inferred_class_name();
  523. //infer from class_name
  524. if (!$this->foreign_key)
  525. $this->foreign_key = array($this->keyify($this->class_name));
  526. $this->primary_key = array(Table::load($this->class_name)->pk[0]);
  527. }
  528. public function load(Model $model)
  529. {
  530. $keys = array();
  531. $inflector = Inflector::instance();
  532. foreach ($this->foreign_key as $key)
  533. $keys[] = $inflector->variablize($key);
  534. if (!($conditions = $this->create_conditions_from_keys($model, $this->primary_key, $keys)))
  535. return null;
  536. $options = $this->unset_non_finder_options($this->options);
  537. $options['conditions'] = $conditions;
  538. $class = $this->class_name;
  539. return $class::first($options);
  540. }
  541. public function load_eagerly($models=array(), $attributes, $includes, Table $table)
  542. {
  543. $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes, $this->primary_key,$this->foreign_key);
  544. }
  545. };
  546. ?>