PageRenderTime 59ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/data/Model.php

https://github.com/Daikoun/lithium
PHP | 1143 lines | 490 code | 82 blank | 571 comment | 68 complexity | 6d36d25544cc95d964df87880dc515ed MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\data;
  9. use lithium\util\Set;
  10. use lithium\util\Inflector;
  11. use lithium\core\ConfigException;
  12. use BadMethodCallException;
  13. /**
  14. * The `Model` class is the starting point for the domain logic of your application.
  15. * Models are tasked with providing meaning to otherwise raw and unprocessed data (e.g.
  16. * user profile).
  17. *
  18. * Models expose a consistent and unified API to interact with an underlying datasource (e.g.
  19. * MongoDB, CouchDB, MySQL) for operations such as querying, saving, updating and deleting data
  20. * from the persistent storage.
  21. *
  22. * Models allow you to interact with your data in two fundamentally different ways: querying, and
  23. * data mutation (saving/updating/deleting). All query-related operations may be done through the
  24. * static `find()` method, along with some additional utility methods provided for convenience.
  25. *
  26. * Classes extending this one should, conventionally, be named as Plural, CamelCase and be
  27. * placed in the `models` directory. i.e. a posts model would be `model/Posts.php`.
  28. *
  29. * Examples:
  30. * {{{
  31. * // Return all 'post' records
  32. * Posts::find('all');
  33. * Posts::all();
  34. *
  35. * // With conditions and a limit
  36. * Posts::find('all', array('conditions' => array('published' => true), 'limit' => 10));
  37. * Posts::all(array('conditions' => array('published' => true), 'limit' => 10));
  38. *
  39. * // Integer count of all 'post' records
  40. * Posts::find('count');
  41. * Posts::count(); // This is equivalent to the above.
  42. *
  43. * // With conditions
  44. * Posts::find('count', array('conditions' => array('published' => true)));
  45. * Posts::count(array('published' => true));
  46. * }}}
  47. *
  48. * The actual objects returned from `find()` calls will depend on the type of data source in use.
  49. * MongoDB, for example, will return results as a `Document` (as will CouchDB), while MySQL will
  50. * return results as a `RecordSet`. Both of these classes extend a common `lithium\data\Collection`
  51. * class, and provide the necessary abstraction to make working with either type completely
  52. * transparent.
  53. *
  54. * For data mutation (saving/updating/deleting), the `Model` class acts as a broker to the proper
  55. * objects. When creating a new record or document, for example, a call to `Posts::create()` will
  56. * return an instance of `lithium\data\entity\Record` or `lithium\data\entity\Document`, which can
  57. * then be acted upon.
  58. *
  59. * Example:
  60. * {{{
  61. * $post = Posts::create();
  62. * $post->author = 'Robert';
  63. * $post->title = 'Newest Post!';
  64. * $post->content = 'Lithium rocks. That is all.';
  65. *
  66. * $post->save();
  67. * }}}
  68. *
  69. * @see lithium\data\entity\Record
  70. * @see lithium\data\entity\Document
  71. * @see lithium\data\collection\RecordSet
  72. * @see lithium\data\collection\DocumentSet
  73. * @see lithium\data\Connections
  74. */
  75. class Model extends \lithium\core\StaticObject {
  76. /**
  77. * Criteria for data validation.
  78. *
  79. * Example usage:
  80. * {{{
  81. * public $validates = array(
  82. * 'title' => 'please enter a title',
  83. * 'email' => array(
  84. * array('notEmpty', 'message' => 'Email is empty.'),
  85. * array('email', 'message' => 'Email is not valid.'),
  86. * )
  87. * );
  88. * }}}
  89. *
  90. * @var array
  91. */
  92. public $validates = array();
  93. /**
  94. * Model hasOne relations.
  95. * Not yet implemented.
  96. *
  97. * @var array
  98. */
  99. public $hasOne = array();
  100. /**
  101. * Model hasMany relations.
  102. * Not yet implemented.
  103. *
  104. * @var array
  105. */
  106. public $hasMany = array();
  107. /**
  108. * Model belongsTo relations.
  109. * Not yet implemented.
  110. *
  111. * @var array
  112. */
  113. public $belongsTo = array();
  114. /**
  115. * Stores model instances for internal use.
  116. *
  117. * While the `Model` public API does not require instantiation thanks to late static binding
  118. * introduced in PHP 5.3, LSB does not apply to class attributes. In order to prevent you
  119. * from needing to redeclare every single `Model` class attribute in subclasses, instances of
  120. * the models are stored and used internally.
  121. *
  122. * @var array
  123. */
  124. protected static $_instances = array();
  125. /**
  126. * Stores the filters that are applied to the model instances stored in `Model::$_instances`.
  127. *
  128. * @var array
  129. */
  130. protected $_instanceFilters = array();
  131. /**
  132. * Class dependencies.
  133. *
  134. * @var array
  135. */
  136. protected static $_classes = array(
  137. 'connections' => 'lithium\data\Connections',
  138. 'query' => 'lithium\data\model\Query',
  139. 'validator' => 'lithium\util\Validator'
  140. );
  141. /**
  142. * A list of the current relation types for this `Model`.
  143. *
  144. * @var array
  145. */
  146. protected $_relations = array();
  147. /**
  148. * List of relation types.
  149. *
  150. * Valid relation types are:
  151. *
  152. * - `belongsTo`
  153. * - `hasOne`
  154. * - `hasMany`
  155. *
  156. * @var array
  157. */
  158. protected $_relationTypes = array('belongsTo', 'hasOne', 'hasMany');
  159. /**
  160. * Specifies all meta-information for this model class, including the name of the data source it
  161. * connects to, how it interacts with that class, and how its data structure is defined.
  162. *
  163. * - `connection`: The name of the connection (as defined in `Connections::add()`) to which the
  164. * model should bind
  165. * - `key`: The primary key or identifier key for records / documents this model produces,
  166. * i.e. `'id'` or `array('_id', '_rev')`. Defaults to `'id'`.
  167. * - `name`: The canonical name of this model. Defaults to the class name.
  168. * - `source`: The name of the database table or document collection to bind to. Defaults to the
  169. * lower-cased and underscored name of the class, i.e. `class UserProfile` maps to
  170. * `'user_profiles'`.
  171. * - `title`: The field or key used as the title for each record. Defaults to `'title'` or
  172. * `'name'`, if those fields are available.
  173. *
  174. * @var array
  175. * @see lithium\data\Connections::add()
  176. */
  177. protected $_meta = array(
  178. 'name' => null,
  179. 'title' => null,
  180. 'class' => null,
  181. 'source' => null,
  182. 'connection' => 'default',
  183. 'initialized' => false
  184. );
  185. /**
  186. * Stores the data schema.
  187. *
  188. * The schema is lazy-loaded by the first call to `Model::schema()`, unless it has been
  189. * manually defined in the `Model` subclass.
  190. *
  191. * For schemaless persistent storage (e.g. MongoDB), this is never populated automatically - if
  192. * you desire a fixed schema to interact with in those cases, you will be required to define it
  193. * yourself.
  194. *
  195. * Example:
  196. * {{{
  197. * protected $_schema = array(
  198. * '_id' => array('type' => 'id'), // required for Mongo
  199. * 'name' => array('type' => 'string', 'default' => 'Moe', 'null' => false),
  200. * 'sign' => array('type' => 'string', 'default' => 'bar', 'null' => false),
  201. * 'age' => array('type' => 'integer', 'default' => 0, 'null' => false)
  202. * );
  203. * }}}
  204. *
  205. * For MongoDB specifically, you can also implement a callback in your database connection
  206. * configuration that fetches and returns the schema data, as in the following:
  207. *
  208. * {{{
  209. * // config/bootstrap/connections.php:
  210. * Connections::add('default', array(
  211. * 'type' => 'MongoDb',
  212. * 'host' => 'localhost',
  213. * 'database' => 'app_name',
  214. * 'schema' => function($db, $collection, $meta) {
  215. * $result = $db->connection->schemas->findOne(compact('collection'));
  216. * return $result ? $result['data'] : array();
  217. * }
  218. * ));
  219. * }}}
  220. *
  221. * This example defines an optional MongoDB convention in which the schema for each individual
  222. * collection is stored in a "schemas" collection, where each document contains the name of
  223. * a collection, along with a `'data'` key, which contains the schema for that collection, in
  224. * the format specified above.
  225. *
  226. * @see lithium\data\source\MongoDb::$_schema
  227. * @var array
  228. */
  229. protected $_schema = array();
  230. /**
  231. * Default query parameters.
  232. *
  233. * - `'conditions'`: The conditional query elements, e.g.
  234. * `'conditions' => array('published' => true)`
  235. * - `'fields'`: The fields that should be retrieved. When set to `null`, defaults to
  236. * all fields.
  237. * - `'order'`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
  238. * - `'limit'`: The maximum number of records to return.
  239. * - `'page'`: For pagination of data.
  240. * - `'with'`: An array of relationship names to be included in the query.
  241. *
  242. * @var array
  243. */
  244. protected $_query = array(
  245. 'conditions' => null,
  246. 'fields' => null,
  247. 'order' => null,
  248. 'limit' => null,
  249. 'page' => null,
  250. 'with' => array()
  251. );
  252. /**
  253. * Custom find query properties, indexed by name.
  254. *
  255. * @var array
  256. */
  257. protected $_finders = array();
  258. /**
  259. * List of base model classes. Any classes which are declared to be base model classes (i.e.
  260. * extended but not directly interacted with) must be present in this list. Models can declare
  261. * themselves as base models using the following code:
  262. * {{{
  263. * public static function __init() {
  264. * static::_isBase(__CLASS__, true);
  265. * parent::__init();
  266. * }
  267. * }}}
  268. *
  269. * @var array
  270. */
  271. protected static $_baseClasses = array(__CLASS__ => true);
  272. /**
  273. * Stores all custom instance methods created by `Model::instanceMethods`.
  274. *
  275. * @var array
  276. */
  277. protected static $_instanceMethods = array();
  278. /**
  279. * Sets default connection options and connects default finders.
  280. *
  281. * @param array $options
  282. * @return void
  283. */
  284. public static function __init() {
  285. static::config();
  286. }
  287. /**
  288. * Configures the model for use. This method is called by `Model::__init()`.
  289. *
  290. * This method will set the `Model::$_schema`, `Model::$_meta`, `Model::$_finders` class
  291. * attributes, as well as obtain a handle to the configured persistent storage connection.
  292. *
  293. * @param array $options Possible options are:
  294. * - `meta`: Meta-information for this model, such as the connection.
  295. * - `finders`: Custom finders for this model.
  296. * @return void
  297. */
  298. public static function config(array $options = array()) {
  299. if (static::_isBase($class = get_called_class())) {
  300. return;
  301. }
  302. $self = static::_object();
  303. $query = array();
  304. $meta = array();
  305. $schema = array();
  306. $source = array();
  307. $classes = static::$_classes;
  308. foreach (static::_parents() as $parent) {
  309. $parentConfig = get_class_vars($parent);
  310. foreach (array('meta', 'schema', 'classes', 'query') as $key) {
  311. if (isset($parentConfig["_{$key}"])) {
  312. ${$key} += $parentConfig["_{$key}"];
  313. }
  314. }
  315. if ($parent == __CLASS__) {
  316. break;
  317. }
  318. }
  319. $tmp = $options + $self->_meta + $meta;
  320. $source = array('meta' => array(), 'finders' => array(), 'schema' => array());
  321. if ($tmp['connection']) {
  322. $conn = $classes['connections']::get($tmp['connection']);
  323. $source = (($conn) ? $conn->configureClass($class) : array()) + $source;
  324. }
  325. static::$_classes = $classes;
  326. $name = static::_name();
  327. $local = compact('class', 'name') + $options + $self->_meta;
  328. $self->_meta = ($local + $source['meta'] + $meta);
  329. $self->_meta['initialized'] = false;
  330. $self->schema()->append($schema + $source['schema']);
  331. $self->_finders += $source['finders'] + $self->_findFilters();
  332. static::_relations();
  333. }
  334. /**
  335. * Allows the use of syntactic-sugar like `Model::all()` instead of `Model::find('all')`.
  336. *
  337. * @see lithium\data\Model::find()
  338. * @see lithium\data\Model::$_meta
  339. * @link http://php.net/manual/en/language.oop5.overloading.php PHP Manual: Overloading
  340. *
  341. * @throws BadMethodCallException On unhandled call, will throw an exception.
  342. * @param string $method Method name caught by `__callStatic()`.
  343. * @param array $params Arguments given to the above `$method` call.
  344. * @return mixed Results of dispatched `Model::find()` call.
  345. */
  346. public static function __callStatic($method, $params) {
  347. $self = static::_object();
  348. $isFinder = isset($self->_finders[$method]);
  349. if ($isFinder && count($params) === 2 && is_array($params[1])) {
  350. $params = array($params[1] + array($method => $params[0]));
  351. }
  352. if ($method == 'all' || $isFinder) {
  353. if ($params && !is_array($params[0])) {
  354. $params[0] = array('conditions' => static::key($params[0]));
  355. }
  356. return $self::find($method, $params ? $params[0] : array());
  357. }
  358. preg_match('/^findBy(?P<field>\w+)$|^find(?P<type>\w+)By(?P<fields>\w+)$/', $method, $args);
  359. if (!$args) {
  360. $message = "Method `%s` not defined or handled in class `%s`.";
  361. throw new BadMethodCallException(sprintf($message, $method, get_class($self)));
  362. }
  363. $field = Inflector::underscore($args['field'] ? $args['field'] : $args['fields']);
  364. $type = isset($args['type']) ? $args['type'] : 'first';
  365. $type[0] = strtolower($type[0]);
  366. $conditions = array($field => array_shift($params));
  367. $params = (isset($params[0]) && count($params) == 1) ? $params[0] : $params;
  368. return $self::find($type, compact('conditions') + $params);
  369. }
  370. /**
  371. * The `find` method allows you to retrieve data from the connected data source.
  372. *
  373. * Examples:
  374. * {{{
  375. * Model::find('all'); // returns all records
  376. * Model::find('count'); // returns a count of all records
  377. *
  378. * // The first ten records that have 'author' set to 'Lithium'
  379. * Model::find('all', array(
  380. * 'conditions' => array('author' => "Lithium"), 'limit' => 10
  381. * ));
  382. * }}}
  383. *
  384. * @param string $type The find type, which is looked up in `Model::$_finders`. By default it
  385. * accepts `all`, `first`, `list` and `count`,
  386. * @param array $options Options for the query. By default, accepts:
  387. * - `conditions`: The conditional query elements, e.g.
  388. * `'conditions' => array('published' => true)`
  389. * - `fields`: The fields that should be retrieved. When set to `null`, defaults to
  390. * all fields.
  391. * - `order`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
  392. * - `limit`: The maximum number of records to return.
  393. * - `page`: For pagination of data.
  394. * @return mixed
  395. * @filter This method can be filtered.
  396. */
  397. public static function find($type, array $options = array()) {
  398. $self = static::_object();
  399. $finder = array();
  400. if ($type === null) {
  401. return null;
  402. }
  403. $isFinder = is_string($type) && isset($self->_finders[$type]);
  404. if ($type != 'all' && !is_array($type) && !$isFinder) {
  405. $options['conditions'] = static::key($type);
  406. $type = 'first';
  407. }
  408. if ($isFinder && is_array($self->_finders[$type])) {
  409. $options = Set::merge($self->_finders[$type], $options);
  410. }
  411. $options = (array) $options + (array) $self->_query;
  412. $meta = array('meta' => $self->_meta, 'name' => get_called_class());
  413. $params = compact('type', 'options');
  414. $filter = function($self, $params) use ($meta) {
  415. $options = $params['options'] + array('type' => 'read', 'model' => $meta['name']);
  416. $query = $self::invokeMethod('_instance', array('query', $options));
  417. return $self::connection()->read($query, $options);
  418. };
  419. if (is_string($type) && isset($self->_finders[$type])) {
  420. $finder = is_callable($self->_finders[$type]) ? array($self->_finders[$type]) : array();
  421. }
  422. return static::_filter(__FUNCTION__, $params, $filter, $finder);
  423. }
  424. /**
  425. * Gets or sets a finder by name. This can be an array of default query options,
  426. * or a closure that accepts an array of query options, and a closure to execute.
  427. *
  428. * @param string $name The finder name, e.g. `first`.
  429. * @param string $options If you are setting a finder, this is the finder definition.
  430. * @return mixed Finder definition if querying, null otherwise.
  431. */
  432. public static function finder($name, $options = null) {
  433. $self = static::_object();
  434. if (empty($options)) {
  435. return isset($self->_finders[$name]) ? $self->_finders[$name] : null;
  436. }
  437. $self->_finders[$name] = $options;
  438. }
  439. /**
  440. * Set/get method for `Model` metadata.
  441. *
  442. * @see lithium\data\Model::$_meta
  443. * @param string $key Model metadata key.
  444. * @param string $value Model metadata value.
  445. * @return mixed Metadata value for a given key.
  446. */
  447. public static function meta($key = null, $value = null) {
  448. $self = static::_object();
  449. if ($value) {
  450. $self->_meta[$key] = $value;
  451. }
  452. if (is_array($key)) {
  453. $self->_meta = $key + $self->_meta;
  454. }
  455. if (!$self->_meta['initialized']) {
  456. $self->_meta['initialized'] = true;
  457. if ($self->_meta['source'] === null) {
  458. $self->_meta['source'] = Inflector::tableize($self->_meta['name']);
  459. }
  460. $titleKeys = array('title', 'name');
  461. if (isset($self->_meta['key'])) {
  462. $titleKeys = array_merge($titleKeys, (array) $self->_meta['key']);
  463. }
  464. $self->_meta['title'] = $self->_meta['title'] ?: static::hasField($titleKeys);
  465. }
  466. if (is_array($key) || !$key || $value) {
  467. return $self->_meta;
  468. }
  469. return isset($self->_meta[$key]) ? $self->_meta[$key] : null;
  470. }
  471. /**
  472. * The `title()` method is invoked whenever an `Entity` object is cast or coerced
  473. * to a string. This method can also be called on the entity directly, i.e. `$post->title()`.
  474. *
  475. * By default, when generating the title for an object, it uses the the field specified in
  476. * the `'title'` key of the model's meta data definition. Override this method to generate
  477. * custom titles for objects of this model's type.
  478. *
  479. * @see lithium\data\Model::$_meta
  480. * @see lithium\data\Entity::__toString()
  481. * @param object $entity The `Entity` instance on which the title method is called.
  482. * @return string Returns the title representation of the entity on which this method is called.
  483. */
  484. public function title($entity) {
  485. $field = static::meta('title');
  486. return $entity->{$field};
  487. }
  488. /**
  489. * If no values supplied, returns the name of the `Model` key. If values
  490. * are supplied, returns the key value.
  491. *
  492. * @param array $values An array of values.
  493. * @return mixed Key value.
  494. */
  495. public static function key($values = array()) {
  496. $key = static::meta('key');
  497. if (is_object($values) && method_exists($values, 'to')) {
  498. $values = $values->to('array');
  499. } elseif (is_object($values) && is_string($key) && isset($values->{$key})) {
  500. return $values->{$key};
  501. }
  502. if (!$values) {
  503. return $key;
  504. }
  505. if (!is_array($values) && !is_array($key)) {
  506. return array($key => $values);
  507. }
  508. $key = (array) $key;
  509. return array_intersect_key($values, array_combine($key, $key));
  510. }
  511. /**
  512. * Returns a list of models related to `Model`, or a list of models related
  513. * to this model, but of a certain type.
  514. *
  515. * @param string $name A type of model relation.
  516. * @return array An array of relation types.
  517. */
  518. public static function relations($name = null) {
  519. $self = static::_object();
  520. if (!$name) {
  521. return $self->_relations;
  522. }
  523. if (in_array($name, $self->_relationTypes)) {
  524. return array_keys(array_filter($self->_relations, function($i) use ($name) {
  525. return $i->data('type') == $name;
  526. }));
  527. }
  528. return isset($self->_relations[$name]) ? $self->_relations[$name] : null;
  529. }
  530. /**
  531. * Creates a relationship binding between this model and another.
  532. *
  533. * @see lithium\data\model\Relationship
  534. * @param string $type The type of relationship to create. Must be one of `'hasOne'`,
  535. * `'hasMany'` or `'belongsTo'`.
  536. * @param string $name The name of the relationship. If this is also the name of the model,
  537. * the model must be in the same namespace as this model. Otherwise, the
  538. * fully-namespaced path to the model class must be specified in `$config`.
  539. * @param array $config Any other configuration that should be specified in the relationship.
  540. * See the `Relationship` class for more information.
  541. * @return object Returns an instance of the `Relationship` class that defines the connection.
  542. */
  543. public static function bind($type, $name, array $config = array()) {
  544. $self = static::_object();
  545. if (!in_array($type, $self->_relationTypes)) {
  546. throw new ConfigException("Invalid relationship type `{$type}` specified.");
  547. }
  548. $rel = static::connection()->relationship(get_called_class(), $type, $name, $config);
  549. return $self->_relations[$name] = $rel;
  550. }
  551. /**
  552. * Lazy-initialize the schema for this Model object, if it is not already manually set in the
  553. * object. You can declare `protected $_schema = array(...)` to define the schema manually.
  554. *
  555. * @param mixed $field Optional. You may pass a field name to get schema information for just
  556. * one field. Otherwise, an array containing all fields is returned. If `false`, the
  557. * schema is reset to an empty value. If an array, field definitions contained are
  558. * appended to the schema.
  559. * @return array
  560. */
  561. public static function schema($field = null) {
  562. $self = static::_object();
  563. $source = $self::meta('source');
  564. if (!is_object($self->_schema)) {
  565. $self->_schema = static::connection()->describe($source, $self->_schema, $self->_meta);
  566. if (!is_object($self->_schema)) {
  567. $class = get_called_class();
  568. throw new ConfigException("Could not load schema object for model `{$class}`.");
  569. }
  570. $key = (array) self::meta('key');
  571. if ($self->_schema && $self->_schema->fields() && !$self->_schema->has($key)) {
  572. $key = implode('`, `', $key);
  573. throw new ConfigException("Missing key `{$key}` from schema.");
  574. }
  575. }
  576. if ($field === false) {
  577. return $self->_schema->reset();
  578. }
  579. if (is_array($field)) {
  580. return $self->_schema->append($field);
  581. }
  582. return $field ? $self->_schema->fields($field) : $self->_schema;
  583. }
  584. /**
  585. * Checks to see if a particular field exists in a model's schema. Can check a single field, or
  586. * return the first field found in an array of multiple options.
  587. *
  588. * @param mixed $field A single field (string) or list of fields (array) to check the existence
  589. * of.
  590. * @return mixed If `$field` is a string, returns a boolean indicating whether or not that field
  591. * exists. If `$field` is an array, returns the first field found, or `false` if none of
  592. * the fields in the list are found.
  593. */
  594. public static function hasField($field) {
  595. if (is_array($field)) {
  596. foreach ($field as $f) {
  597. if (static::hasField($f)) {
  598. return $f;
  599. }
  600. }
  601. return false;
  602. }
  603. $schema = static::schema();
  604. return ($schema && isset($schema[$field]));
  605. }
  606. /**
  607. * Instantiates a new record or document object, initialized with any data passed in. For
  608. * example:
  609. *
  610. * {{{
  611. * $post = Posts::create(array("title" => "New post"));
  612. * echo $post->title; // echoes "New post"
  613. * $success = $post->save();
  614. * }}}
  615. *
  616. * Note that while this method creates a new object, there is no effect on the database until
  617. * the `save()` method is called.
  618. *
  619. * In addition, this method can be used to simulate loading a pre-existing object from the
  620. * database, without actually querying the database:
  621. *
  622. * {{{
  623. * $post = Posts::create(array("id" => $id, "moreData" => "foo"), array("exists" => true));
  624. * $post->title = "New title";
  625. * $success = $post->save();
  626. * }}}
  627. *
  628. * This will create an update query against the object with an ID matching `$id`. Also note that
  629. * only the `title` field will be updated.
  630. *
  631. * @param array $data Any data that this object should be populated with initially.
  632. * @param array $options Options to be passed to item.
  633. * @return object Returns a new, _un-saved_ record or document object. In addition to the values
  634. * passed to `$data`, the object will also contain any values assigned to the
  635. * `'default'` key of each field defined in `$_schema`.
  636. * @filter
  637. */
  638. public static function create(array $data = array(), array $options = array()) {
  639. return static::_filter(__FUNCTION__, compact('data', 'options'), function($self, $params) {
  640. $data = Set::merge(Set::expand($self::schema()->defaults()), $params['data']);
  641. return $self::connection()->item($self, $data, $params['options']);
  642. });
  643. }
  644. /**
  645. * Getter and setter for custom instance methods. This is used in `Entity::__call()`.
  646. *
  647. * {{{
  648. * Model::instanceMethods(array(
  649. * 'methodName' => array('Class', 'method'),
  650. * 'anotherMethod' => array($object, 'method'),
  651. * 'closureCallback' => function($entity) {}
  652. * ));
  653. * }}}
  654. *
  655. * @see lithium\data\Entity::__call()
  656. * @param array $methods
  657. * @return array
  658. */
  659. public static function instanceMethods(array $methods = null) {
  660. $class = get_called_class();
  661. if (!isset(static::$_instanceMethods[$class])) {
  662. static::$_instanceMethods[$class] = array();
  663. }
  664. if ($methods === array()) {
  665. return static::$_instanceMethods[$class] = array();
  666. }
  667. if (!is_null($methods)) {
  668. static::$_instanceMethods[$class] = $methods + static::$_instanceMethods[$class];
  669. }
  670. return static::$_instanceMethods[$class];
  671. }
  672. /**
  673. * An instance method (called on record and document objects) to create or update the record or
  674. * document in the database that corresponds to `$entity`.
  675. *
  676. * For example, to create a new record or document:
  677. * {{{
  678. * $post = Posts::create(); // Creates a new object, which doesn't exist in the database yet
  679. * $post->title = "My post";
  680. * $success = $post->save();
  681. * }}}
  682. *
  683. * It is also used to update existing database objects, as in the following:
  684. * {{{
  685. * $post = Posts::first($id);
  686. * $post->title = "Revised title";
  687. * $success = $post->save();
  688. * }}}
  689. *
  690. * By default, an object's data will be checked against the validation rules of the model it is
  691. * bound to. Any validation errors that result can then be accessed through the `errors()`
  692. * method.
  693. *
  694. * {{{
  695. * if (!$post->save($someData)) {
  696. * return array('errors' => $post->errors());
  697. * }
  698. * }}}
  699. *
  700. * To override the validation checks and save anyway, you can pass the `'validate'` option:
  701. *
  702. * {{{
  703. * $post->title = "We Don't Need No Stinkin' Validation";
  704. * $post->body = "I know what I'm doing.";
  705. * $post->save(null, array('validate' => false));
  706. * }}}
  707. *
  708. * @see lithium\data\Model::$validates
  709. * @see lithium\data\Model::validates()
  710. * @see lithium\data\Model::errors()
  711. * @param object $entity The record or document object to be saved in the database. This
  712. * parameter is implicit and should not be passed under normal circumstances.
  713. * In the above example, the call to `save()` on the `$post` object is
  714. * transparently proxied through to the `Posts` model class, and `$post` is passed
  715. * in as the `$entity` parameter.
  716. * @param array $data Any data that should be assigned to the record before it is saved.
  717. * @param array $options Options:
  718. * - `'callbacks'` _boolean_: If `false`, all callbacks will be disabled before
  719. * executing. Defaults to `true`.
  720. * - `'validate'` _mixed_: If `false`, validation will be skipped, and the record will
  721. * be immediately saved. Defaults to `true`. May also be specified as an array, in
  722. * which case it will replace the default validation rules specified in the
  723. * `$validates` property of the model.
  724. * - `'events'` _mixed_: A string or array defining one or more validation _events_.
  725. * Events are different contexts in which data events can occur, and correspond to the
  726. * optional `'on'` key in validation rules. They will be passed to the validates()
  727. * method if `'validate'` is not `false`.
  728. * - `'whitelist'` _array_: An array of fields that are allowed to be saved to this
  729. * record.
  730. *
  731. * @return boolean Returns `true` on a successful save operation, `false` on failure.
  732. * @filter
  733. */
  734. public function save($entity, $data = null, array $options = array()) {
  735. $self = static::_object();
  736. $_meta = array('model' => get_called_class()) + $self->_meta;
  737. $_schema = $self->_schema;
  738. $defaults = array(
  739. 'validate' => true,
  740. 'events' => $entity->exists() ? 'update' : 'create',
  741. 'whitelist' => null,
  742. 'callbacks' => true,
  743. 'locked' => $self->_meta['locked']
  744. );
  745. $options += $defaults;
  746. $params = compact('entity', 'data', 'options');
  747. $filter = function($self, $params) use ($_meta, $_schema) {
  748. $entity = $params['entity'];
  749. $options = $params['options'];
  750. if ($params['data']) {
  751. $entity->set($params['data']);
  752. }
  753. if ($rules = $options['validate']) {
  754. $events = $options['events'];
  755. $validateOpts = is_array($rules) ? compact('rules','events') : compact('events');
  756. if (!$entity->validates($validateOpts)) {
  757. return false;
  758. }
  759. }
  760. if (($whitelist = $options['whitelist']) || $options['locked']) {
  761. $whitelist = $whitelist ?: array_keys($_schema->fields());
  762. }
  763. $type = $entity->exists() ? 'update' : 'create';
  764. $queryOpts = compact('type', 'whitelist', 'entity') + $options + $_meta;
  765. $query = $self::invokeMethod('_instance', array('query', $queryOpts));
  766. return $self::connection()->{$type}($query, $options);
  767. };
  768. if (!$options['callbacks']) {
  769. return $filter(get_called_class(), $params);
  770. }
  771. return static::_filter(__FUNCTION__, $params, $filter);
  772. }
  773. /**
  774. * An important part of describing the business logic of a model class is defining the
  775. * validation rules. In Lithium models, rules are defined through the `$validates` class
  776. * property, and are used by this method before saving to verify the correctness of the data
  777. * being sent to the backend data source.
  778. *
  779. * Note that these are application-level validation rules, and do not
  780. * interact with any rules or constraints defined in your data source. If such constraints fail,
  781. * an exception will be thrown by the database layer. The `validates()` method only checks
  782. * against the rules defined in application code.
  783. *
  784. * This method uses the `Validator` class to perform data validation. An array representation of
  785. * the entity object to be tested is passed to the `check()` method, along with the model's
  786. * validation rules. Any rules defined in the `Validator` class can be used to validate fields.
  787. * See the `Validator` class to add custom rules, or override built-in rules.
  788. *
  789. * @see lithium\data\Model::$validates
  790. * @see lithium\util\Validator::check()
  791. * @see lithium\data\Entity::errors()
  792. * @param string $entity Model entity to validate. Typically either a `Record` or `Document`
  793. * object. In the following example:
  794. * {{{
  795. * $post = Posts::create($data);
  796. * $success = $post->validates();
  797. * }}}
  798. * The `$entity` parameter is equal to the `$post` object instance.
  799. * @param array $options Available options:
  800. * - `'rules'` _array_: If specified, this array will _replace_ the default
  801. * validation rules defined in `$validates`.
  802. * - `'events'` _mixed_: A string or array defining one or more validation
  803. * _events_. Events are different contexts in which data events can occur, and
  804. * correspond to the optional `'on'` key in validation rules. For example, by
  805. * default, `'events'` is set to either `'create'` or `'update'`, depending on
  806. * whether `$entity` already exists. Then, individual rules can specify
  807. * `'on' => 'create'` or `'on' => 'update'` to only be applied at certain times.
  808. * Using this parameter, you can set up custom events in your rules as well, such
  809. * as `'on' => 'login'`. Note that when defining validation rules, the `'on'` key
  810. * can also be an array of multiple events.
  811. * @return boolean Returns `true` if all validation rules on all fields succeed, otherwise
  812. * `false`. After validation, the messages for any validation failures are assigned to
  813. * the entity, and accessible through the `errors()` method of the entity object.
  814. * @filter
  815. */
  816. public function validates($entity, array $options = array()) {
  817. $defaults = array(
  818. 'rules' => $this->validates,
  819. 'events' => $entity->exists() ? 'update' : 'create',
  820. 'model' => get_called_class()
  821. );
  822. $options += $defaults;
  823. $self = static::_object();
  824. $validator = static::$_classes['validator'];
  825. $params = compact('entity', 'options');
  826. $filter = function($parent, $params) use (&$self, $validator) {
  827. $entity = $params['entity'];
  828. $options = $params['options'];
  829. $rules = $options['rules'];
  830. unset($options['rules']);
  831. if ($errors = $validator::check($entity->data(), $rules, $options)) {
  832. $entity->errors($errors);
  833. }
  834. return empty($errors);
  835. };
  836. return static::_filter(__FUNCTION__, $params, $filter);
  837. }
  838. /**
  839. * Deletes the data associated with the current `Model`.
  840. *
  841. * @param object $entity Entity to delete.
  842. * @param array $options Options.
  843. * @return boolean Success.
  844. * @filter
  845. */
  846. public function delete($entity, array $options = array()) {
  847. $params = compact('entity', 'options');
  848. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  849. $options = $params + $params['options'] + array('model' => $self, 'type' => 'delete');
  850. unset($options['options']);
  851. $query = $self::invokeMethod('_instance', array('query', $options));
  852. return $self::connection()->delete($query, $options);
  853. });
  854. }
  855. /**
  856. * Update multiple records or documents with the given data, restricted by the given set of
  857. * criteria (optional).
  858. *
  859. * @param mixed $data Typically an array of key/value pairs that specify the new data with which
  860. * the records will be updated. For SQL databases, this can optionally be an SQL
  861. * fragment representing the `SET` clause of an `UPDATE` query.
  862. * @param mixed $conditions An array of key/value pairs representing the scope of the records
  863. * to be updated.
  864. * @param array $options Any database-specific options to use when performing the operation. See
  865. * the `delete()` method of the corresponding backend database for available
  866. * options.
  867. * @return boolean Returns `true` if the update operation succeeded, otherwise `false`.
  868. * @filter
  869. */
  870. public static function update($data, $conditions = array(), array $options = array()) {
  871. $params = compact('data', 'conditions', 'options');
  872. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  873. $options = $params + $params['options'] + array('model' => $self, 'type' => 'update');
  874. unset($options['options']);
  875. $query = $self::invokeMethod('_instance', array('query', $options));
  876. return $self::connection()->update($query, $options);
  877. });
  878. }
  879. /**
  880. * Remove multiple documents or records based on a given set of criteria. **WARNING**: If no
  881. * criteria are specified, or if the criteria (`$conditions`) is an empty value (i.e. an empty
  882. * array or `null`), all the data in the backend data source (i.e. table or collection) _will_
  883. * be deleted.
  884. *
  885. * @param mixed $conditions An array of key/value pairs representing the scope of the records or
  886. * documents to be deleted.
  887. * @param array $options Any database-specific options to use when performing the operation. See
  888. * the `delete()` method of the corresponding backend database for available
  889. * options.
  890. * @return boolean Returns `true` if the remove operation succeeded, otherwise `false`.
  891. * @filter
  892. */
  893. public static function remove($conditions = array(), array $options = array()) {
  894. $params = compact('conditions', 'options');
  895. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  896. $options = $params['options'] + $params + array('model' => $self, 'type' => 'delete');
  897. unset($options['options']);
  898. $query = $self::invokeMethod('_instance', array('query', $options));
  899. return $self::connection()->delete($query, $options);
  900. });
  901. }
  902. /**
  903. * Gets the connection object to which this model is bound. Throws exceptions if a connection
  904. * isn't set, or if the connection named isn't configured.
  905. *
  906. * @return object Returns an instance of `lithium\data\Source` from the connection configuration
  907. * to which this model is bound.
  908. */
  909. public static function &connection() {
  910. $self = static::_object();
  911. $connections = static::$_classes['connections'];
  912. $name = isset($self->_meta['connection']) ? $self->_meta['connection'] : null;
  913. if ($conn = $connections::get($name)) {
  914. return $conn;
  915. }
  916. throw new ConfigException("The data connection `{$name}` is not configured.");
  917. }
  918. /**
  919. * Gets just the class name portion of a fully-name-spaced class name, i.e.
  920. * `app\models\Posts::_name()` returns `'Posts'`.
  921. *
  922. * @return string
  923. */
  924. protected static function _name() {
  925. return basename(str_replace('\\', '/', get_called_class()));
  926. }
  927. /**
  928. * Wraps `StaticObject::applyFilter()` to account for object instances.
  929. *
  930. * @see lithium\core\StaticObject::applyFilter()
  931. * @param string $method
  932. * @param mixed $closure
  933. */
  934. public static function applyFilter($method, $closure = null) {
  935. $instance = static::_object();
  936. $methods = (array) $method;
  937. foreach ($methods as $method) {
  938. if (!isset($instance->_instanceFilters[$method])) {
  939. $instance->_instanceFilters[$method] = array();
  940. }
  941. $instance->_instanceFilters[$method][] = $closure;
  942. }
  943. }
  944. /**
  945. * Wraps `StaticObject::_filter()` to account for object instances.
  946. *
  947. * @see lithium\core\StaticObject::_filter()
  948. * @param string $method
  949. * @param array $params
  950. * @param mixed $callback
  951. * @param array $filters Defaults to empty array.
  952. * @return object
  953. */
  954. protected static function _filter($method, $params, $callback, $filters = array()) {
  955. if (!strpos($method, '::')) {
  956. $method = get_called_class() . '::' . $method;
  957. }
  958. list($class, $method) = explode('::', $method, 2);
  959. $instance = static::_object();
  960. if (isset($instance->_instanceFilters[$method])) {
  961. $filters = array_merge($instance->_instanceFilters[$method], $filters);
  962. }
  963. return parent::_filter($method, $params, $callback, $filters);
  964. }
  965. protected static function &_object() {
  966. $class = get_called_class();
  967. if (!isset(static::$_instances[$class])) {
  968. static::$_instances[$class] = new $class();
  969. }
  970. return static::$_instances[$class];
  971. }
  972. /**
  973. * Iterates through relationship types to construct relation map.
  974. *
  975. * @return void
  976. * @todo See if this can be rewritten to be lazy.
  977. */
  978. protected static function _relations() {
  979. try {
  980. if (!static::connection()) {
  981. return;
  982. }
  983. } catch (ConfigExcepton $e) {
  984. return;
  985. }
  986. $self = static::_object();
  987. foreach ($self->_relationTypes as $type) {
  988. foreach (Set::normalize($self->{$type}) as $name => $config) {
  989. static::bind($type, $name, (array) $config);
  990. }
  991. }
  992. }
  993. /**
  994. * Helper function for setting/getting base class settings.
  995. *
  996. * @param string $class Classname.
  997. * @param boolean $set If `true`, then the `$class` will be set.
  998. * @return boolean Success.
  999. */
  1000. protected static function _isBase($class = null, $set = false) {
  1001. if ($set) {
  1002. static::$_baseClasses[$class] = true;
  1003. }
  1004. return isset(static::$_baseClasses[$class]);
  1005. }
  1006. /**
  1007. * Exports an array of custom finders which use the filter system to wrap around `find()`.
  1008. *
  1009. * @return void
  1010. */
  1011. protected static function _findFilters() {
  1012. $self = static::_object();
  1013. $_query = $self->_query;
  1014. return array(
  1015. 'first' => function($self, $params, $chain) {
  1016. $params['options']['limit'] = 1;
  1017. $data = $chain->next($self, $params, $chain);
  1018. $data = is_object($data) ? $data->rewind() : $data;
  1019. return $data ?: null;
  1020. },
  1021. 'list' => function($self, $params, $chain) {
  1022. $result = array();
  1023. $meta = $self::meta();
  1024. $name = $meta['key'];
  1025. foreach ($chain->next($self, $params, $chain) as $entity) {
  1026. $key = $entity->{$name};
  1027. $result[is_scalar($key) ? $key : (string) $key] = $entity->title();
  1028. }
  1029. return $result;
  1030. },
  1031. 'count' => function($self, $params) use ($_query) {
  1032. $model = $self;
  1033. $type = $params['type'];
  1034. $options = array_diff_key($params['options'], $_query);
  1035. if ($options && !isset($params['options']['conditions'])) {
  1036. $options = array('conditions' => $options);
  1037. } else {
  1038. $options = $params['options'];
  1039. }
  1040. $options += array('type' => 'read') + compact('model');
  1041. $query = $self::invokeMethod('_instance', array('query', $options));
  1042. return $self::connection()->calculation('count', $query, $options);
  1043. }
  1044. );
  1045. }
  1046. }
  1047. ?>