PageRenderTime 59ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/lithium/data/Model.php

https://github.com/prestes/chegamos
PHP | 991 lines | 427 code | 92 blank | 472 comment | 68 complexity | 3d9098a65a96e52e2c9f2cfc4df14ab1 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2010, 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 Singular, CamelCase and be
  27. * placed in the app/models directory. i.e. a posts model would be app/model/Post.php.
  28. *
  29. * Examples:
  30. * {{{
  31. * // Return all 'post' records
  32. * Post::find('all');
  33. * Post::all();
  34. *
  35. * // With conditions and a limit
  36. * Post::find('all', array('conditions' => array('published' => true), 'limit' => 10));
  37. * Post::all(array('conditions' => array('published' => true), 'limit' => 10));
  38. *
  39. * // Integer count of all 'post' records
  40. * Post::find('count');
  41. * Post::count(); // This is equivalent to the above.
  42. *
  43. * // With conditions
  44. * Post::find('count', array('conditions' => array('published' => true)));
  45. * Post::count(array('published' => true));
  46. * }}}
  47. *
  48. * The actual objects returned from `find()` calls will depend on the type of datasource in use.
  49. * MongoDB, for example, will return results as a `Document`, while MySQL will return results
  50. * as a `RecordSet`. Both of these classes extend a common `data\Collection` class, and provide
  51. * the necessary abstraction to make working with either type completely transparent.
  52. *
  53. * For data mutation (saving/updating/deleting), the `Model` class acts as a broker to the proper
  54. * objects. When creating a new record, for example, a call to `Post::create()` will return a
  55. * `data\model\Record` object, which can then be acted upon.
  56. *
  57. * Example:
  58. * {{{
  59. * $post = Post::create();
  60. * $post->author = 'Robert';
  61. * $post->title = 'Newest Post!';
  62. * $post->content = 'Lithium rocks. That is all.';
  63. *
  64. * $post->save();
  65. * }}}
  66. *
  67. * @see lithium\data\entity\Record
  68. * @see lithium\data\entity\Document
  69. * @see lithium\data\collection\RecordSet
  70. * @see lithium\data\collection\DocumentSet
  71. * @see lithium\data\Connections
  72. */
  73. class Model extends \lithium\core\StaticObject {
  74. /**
  75. * Criteria for data validation.
  76. *
  77. * Example usage:
  78. * {{{
  79. * public $validates = array(
  80. * 'title' => 'please enter a title',
  81. * 'email' => array(
  82. * array('notEmpty', 'message' => 'Email is empty.'),
  83. * array('email', 'message' => 'Email is not valid.'),
  84. * )
  85. * );
  86. * }}}
  87. *
  88. * @var array
  89. */
  90. public $validates = array();
  91. /**
  92. * Model hasOne relations.
  93. * Not yet implemented.
  94. *
  95. * @var array
  96. */
  97. public $hasOne = array();
  98. /**
  99. * Model hasMany relations.
  100. * Not yet implemented.
  101. *
  102. * @var array
  103. */
  104. public $hasMany = array();
  105. /**
  106. * Model belongsTo relations.
  107. * Not yet implemented.
  108. *
  109. * @var array
  110. */
  111. public $belongsTo = array();
  112. /**
  113. * Stores model instances for internal use.
  114. *
  115. * While the `Model` public API does not require instantiation thanks to late static binding
  116. * introduced in PHP 5.3, LSB does not apply to class attributes. In order to prevent you
  117. * from needing to redeclare every single `Model` class attribute in subclasses, instances of
  118. * the models are stored and used internally.
  119. *
  120. * @var array
  121. */
  122. protected static $_instances = array();
  123. /**
  124. * Stores the filters that are applied to the model instances stored in `Model::$_instances`.
  125. *
  126. * @var array
  127. */
  128. protected $_instanceFilters = array();
  129. /**
  130. * Class dependencies.
  131. *
  132. * @var array
  133. */
  134. protected $_classes = array(
  135. 'connections' => '\lithium\data\Connections',
  136. 'entity' => '\lithium\data\Entity',
  137. 'query' => '\lithium\data\model\Query',
  138. 'validator' => '\lithium\util\Validator'
  139. );
  140. /**
  141. * A list of the current relation types for this `Model`.
  142. *
  143. * @var array
  144. */
  145. protected $_relations = array();
  146. /**
  147. * List of relation types and the configuration fields that these relations
  148. * require/accept.
  149. *
  150. * Valid relation types are:
  151. *
  152. * - `belongsTo`
  153. * - `hasOne`
  154. * - `hasMany`
  155. *
  156. * @var array
  157. */
  158. protected $_relationTypes = array(
  159. 'belongsTo' => array('class', 'key', 'conditions', 'fields'),
  160. 'hasOne' => array('class', 'key', 'conditions', 'fields', 'dependent'),
  161. 'hasMany' => array(
  162. 'class', 'key', 'conditions', 'fields', 'order', 'limit',
  163. 'dependent', 'exclusive', 'finder', 'counter'
  164. )
  165. );
  166. /**
  167. * Specifies all meta-information for this model class, including the name of the data source it
  168. * connects to, how it interacts with that class, and how its data structure is defined.
  169. *
  170. * - `connection`: The name of the connection (as defined in `Connections::add()`) to which the
  171. * model should bind
  172. * - `key`: The primary key or identifier key for records / documents this model produces,
  173. * i.e. `'id'` or `array('_id', '_rev')`. Defaults to `'id'`.
  174. * - `name`: The canonical name of this model. Defaults to the class name.
  175. * - `source`: The name of the database table or document collection to bind to. Defaults to the
  176. * lower-cased and underscored name of the class, i.e. `class UserProfile` maps to
  177. * `'user_profiles'`.
  178. * - `title`: The field or key used as the title for each record. Defaults to `'title'` or
  179. * `'name'`, if those fields are available.
  180. *
  181. * @var array
  182. * @see lithium\data\Connections::add()
  183. */
  184. protected $_meta = array(
  185. 'key' => 'id',
  186. 'name' => null,
  187. 'title' => null,
  188. 'class' => null,
  189. 'locked' => true,
  190. 'source' => null,
  191. 'connection' => 'default',
  192. 'initialized' => false
  193. );
  194. /**
  195. * Stores the data schema.
  196. *
  197. * The schema is lazy-loaded by the first call to `Model::schema()`, unless it has been
  198. * manually defined in the `Model` subclass.
  199. *
  200. * For schemaless persistent storage (e.g. MongoDB), this is never populated automatically - if
  201. * you desire a fixed schema to interact with in those cases, you will be required to define it
  202. * yourself.
  203. *
  204. * Example:
  205. * {{{
  206. * protected $_schema = array(
  207. * 'name' => array('default' => 'Moe', 'type' => 'string', 'null' => false),
  208. * 'sign' => array('default' => 'bar', 'type' => 'string', 'null' => false),
  209. * 'age' => array('default' => 0, 'type' => 'number', 'null' => false)
  210. * );
  211. * }}}
  212. *
  213. * For MongoDB specifically, you can also implement a callback in your database connection
  214. * configuration that fetches and returns the schema data, as in the following:
  215. *
  216. * {{{
  217. * Connections::add('default', array(
  218. * 'type' => 'MongoDb',
  219. * 'host' => 'localhost',
  220. * 'database' => 'app_name',
  221. * 'schema' => function($db, $collection, $meta) {
  222. * $result = $db->connection->schemas->findOne(compact('collection'));
  223. * return $result ? $result['data'] : array();
  224. * }
  225. * ));
  226. * }}}
  227. *
  228. * This example defines an optional MongoDB convention in which the schema for each individual
  229. * collection is stored in a "schemas" collection, where each document contains the name of
  230. * a collection, along with a `'data'` key, which contains the schema for that collection.
  231. *
  232. * @see lithium\data\source\MongoDb::$_schema
  233. * @var array
  234. */
  235. protected $_schema = array();
  236. /**
  237. * Default query parameters.
  238. *
  239. * - `conditions`: The conditional query elements, e.g.
  240. * `'conditions' => array('published' => true)`
  241. * - `fields`: The fields that should be retrieved. When set to `null`, defaults to
  242. * all fields.
  243. * - `order`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
  244. * - `limit`: The maximum number of records to return.
  245. * - `page`: For pagination of data.
  246. *
  247. * @var array
  248. */
  249. protected $_query = array(
  250. 'conditions' => null,
  251. 'fields' => null,
  252. 'order' => null,
  253. 'limit' => null,
  254. 'page' => null
  255. );
  256. /**
  257. * Custom find query properties, indexed by name.
  258. *
  259. * @var array
  260. */
  261. protected $_finders = array();
  262. /**
  263. * List of base model classes. Any classes which are declared to be base model classes (i.e.
  264. * extended but not directly interacted with) must be present in this list. Models can declare
  265. * themselves as base models using the following code:
  266. * {{{
  267. * public static function __init() {
  268. * static::_isBase(__CLASS__, true);
  269. * parent::__init();
  270. * }
  271. * }}}
  272. *
  273. * @var array
  274. */
  275. protected static $_baseClasses = array(__CLASS__ => true);
  276. /**
  277. * Sets default connection options and connects default finders.
  278. *
  279. * @param array $options
  280. * @return void
  281. * @todo Merge in inherited config from AppModel and other parent classes.
  282. */
  283. public static function __init() {
  284. static::config();
  285. }
  286. /**
  287. * Configures the model for use. This method is called by `Model::__init()`.
  288. *
  289. * This method will set the `Model::$_classes`, `Model::$_meta`, `Model::$_finders` class
  290. * attributes, as well as obtain a handle to the configured persistent storage connection.
  291. *
  292. * @param array $options Possible options are:
  293. * - `classes`: Dynamic class dependencies.
  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. $name = static::_name();
  303. $self = static::_object();
  304. $defaults = array('classes' => array(), 'meta' => array(), 'finders' => array());
  305. $meta = $options + $self->_meta;
  306. $classes = $self->_classes;
  307. $schema = array();
  308. $config = array();
  309. foreach (static::_parents() as $parent) {
  310. $base = get_class_vars($parent);
  311. foreach (array('meta', 'schema', 'classes') as $key) {
  312. if (isset($base["_{$key}"])) {
  313. ${$key} += $base["_{$key}"];
  314. }
  315. }
  316. if ($class == __CLASS__) {
  317. break;
  318. }
  319. }
  320. if ($meta['connection']) {
  321. $conn = $classes['connections']::get($meta['connection']);
  322. $config = ($conn) ? $conn->configureClass($class) : array();
  323. }
  324. $config += $defaults;
  325. $self->_classes = ($config['classes'] + $classes);
  326. $self->_meta = (compact('class', 'name') + $config['meta'] + $meta);
  327. $self->_meta['initialized'] = false;
  328. $self->_schema += $schema;
  329. $self->_finders += $config['finders'] + $self->_findFilters();
  330. static::_relations();
  331. }
  332. /**
  333. * Allows the use of syntactic-sugar like `Model::all()` instead of `Model::find('all')`.
  334. *
  335. * @see lithium\data\Model::find()
  336. * @link http://php.net/manual/en/language.oop5.overloading.php PHP Manual: Overloading
  337. *
  338. * @throws BadMethodCallException On unhandled call, will throw an exception.
  339. * @param string $method Method name caught by `__callStatic`.
  340. * @param array $params Arguments given to the above `$method` call.
  341. * @return mixed Results of dispatched `Model::find()` call.
  342. */
  343. public static function __callStatic($method, $params) {
  344. $self = static::_object();
  345. $isFinder = isset($self->_finders[$method]);
  346. if ($isFinder && count($params) === 2 && is_array($params[1])) {
  347. $params = array($params[1] + array($method => $params[0]));
  348. }
  349. if ($method == 'all' || $isFinder) {
  350. if ($params && is_scalar($params[0])) {
  351. $params[0] = array('conditions' => array($self->_meta['key'] => $params[0]));
  352. }
  353. return $self::find($method, $params ? $params[0] : array());
  354. }
  355. preg_match('/^findBy(?P<field>\w+)$|^find(?P<type>\w+)By(?P<fields>\w+)$/', $method, $args);
  356. if (!$args) {
  357. $message = "Method %s not defined or handled in class %s";
  358. throw new BadMethodCallException(sprintf($message, $method, get_class($self)));
  359. }
  360. $field = Inflector::underscore($args['field'] ? $args['field'] : $args['fields']);
  361. $type = isset($args['type']) ? $args['type'] : 'first';
  362. $type[0] = strtolower($type[0]);
  363. $conditions = array($field => array_shift($params));
  364. return $self::find($type, compact('conditions') + $params);
  365. }
  366. /**
  367. * The `find` method allows you to retrieve data from the connected data source.
  368. *
  369. * Examples:
  370. * {{{
  371. * Model::find('all'); // returns all records
  372. * Model::find('count'); // returns a count of all records
  373. *
  374. * // The first ten records that have 'author' set to 'Lithium'
  375. * Model::find('all', array(
  376. * 'conditions' => array('author' => "Lithium"), 'limit' => 10
  377. * ));
  378. * }}}
  379. *
  380. * @param string $type The find type, which is looked up in `Model::$_finders`. By default it
  381. * accepts `all`, `first`, `list` and `count`,
  382. * @param string $options Options for the query. By default, accepts:
  383. * - `conditions`: The conditional query elements, e.g.
  384. * `'conditions' => array('published' => true)`
  385. * - `fields`: The fields that should be retrieved. When set to `null`, defaults to
  386. * all fields.
  387. * - `order`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
  388. * - `limit`: The maximum number of records to return.
  389. * - `page`: For pagination of data.
  390. * @return void
  391. * @filter This method can be filtered.
  392. */
  393. public static function find($type, array $options = array()) {
  394. $self = static::_object();
  395. $classes = $self->_classes;
  396. $finder = array();
  397. $defaults = array(
  398. 'conditions' => null, 'fields' => null, 'order' => null, 'limit' => null, 'page' => 1
  399. );
  400. if ($type === null) {
  401. return null;
  402. }
  403. if ($type != 'all' && is_scalar($type) && !isset($self->_finders[$type])) {
  404. $options['conditions'] = array($self->_meta['key'] => $type);
  405. $type = 'first';
  406. }
  407. $options += ((array) $self->_query + (array) $defaults + compact('classes'));
  408. $meta = array('meta' => $self->_meta, 'name' => get_called_class());
  409. $params = compact('type', 'options');
  410. $filter = function($self, $params) use ($meta) {
  411. $options = $params['options'] + array('model' => $meta['name']);
  412. $query = $options['classes']['query'];
  413. $connection = $self::invokeMethod('_connection');
  414. return $connection->read(new $query(array('type' => 'read') + $options), $options);
  415. };
  416. if (is_string($type) && isset($self->_finders[$type])) {
  417. $finder = is_callable($self->_finders[$type]) ? array($self->_finders[$type]) : array();
  418. }
  419. return static::_filter(__FUNCTION__, $params, $filter, $finder);
  420. }
  421. /**
  422. * Gets or sets a finder by name. This can be an array of default query options,
  423. * or a closure that accepts an array of query options, and a closure to execute.
  424. *
  425. * @param string $name The finder name, e.g. `first`.
  426. * @param string $options If you are setting a finder, this is the finder definition.
  427. * @return mixed Finder definition if querying, null otherwise.
  428. */
  429. public static function finder($name, $options = null) {
  430. $self = static::_object();
  431. if (empty($options)) {
  432. return isset($self->_finders[$name]) ? $self->_finders[$name] : null;
  433. }
  434. $self->_finders[$name] = $options;
  435. }
  436. /**
  437. * Set/get method for `Model` metadata.
  438. *
  439. * @see lithium\data\Model::$_meta
  440. * @param string $key Model metadata key.
  441. * @param string $value Model metadata value.
  442. * @return mixed Metadata value for a given key.
  443. */
  444. public static function meta($key = null, $value = null) {
  445. $self = static::_object();
  446. if ($value) {
  447. $self->_meta[$key] = $value;
  448. }
  449. if (is_array($key)) {
  450. $self->_meta = $key + $self->_meta;
  451. }
  452. if (!$self->_meta['initialized']) {
  453. $self->_meta['initialized'] = true;
  454. if ($self->_meta['source'] === null) {
  455. $self->_meta['source'] = Inflector::tableize($self->_meta['name']);
  456. }
  457. $titleKeys = array('title', 'name', $self->_meta['key']);
  458. $self->_meta['title'] = $self->_meta['title'] ?: static::hasField($titleKeys);
  459. }
  460. if (is_array($key) || empty($key) || !empty($value)) {
  461. return $self->_meta;
  462. }
  463. return isset($self->_meta[$key]) ? $self->_meta[$key] : null;
  464. }
  465. /**
  466. * If no values supplied, returns the name of the `Model` key. If values
  467. * are supplied, returns the key value.
  468. *
  469. * @param array $values An array of values.
  470. * @return mixed Key value.
  471. */
  472. public static function key($values = array()) {
  473. $key = static::_object()->_meta['key'];
  474. if (is_object($values) && method_exists($values, 'to')) {
  475. $values = $values->to('array');
  476. } elseif (is_object($values) && is_string($key) && isset($values->{$key})) {
  477. return $values->{$key};
  478. }
  479. if (!$values) {
  480. return $key;
  481. }
  482. if (!is_array($values) && !is_array($key)) {
  483. return array($key => $values);
  484. }
  485. $key = (array) $key;
  486. return array_intersect_key($values, array_combine($key, $key));
  487. }
  488. /**
  489. * Returns a list of models related to `Model`, or a list of models related
  490. * to this model, but of a certain type.
  491. *
  492. * @param string $name A type of model relation.
  493. * @return array An array of relation types.
  494. */
  495. public static function relations($name = null) {
  496. $self = static::_object();
  497. if (!$name) {
  498. return $self->_relations;
  499. }
  500. if (isset($self->_relationTypes[$name])) {
  501. return array_keys(array_filter($self->_relations, function($i) use ($name) {
  502. return $i->data('type') == $name;
  503. }));
  504. }
  505. return isset($self->_relations[$name]) ? $self->_relations[$name] : null;
  506. }
  507. /**
  508. * Creates a relationship binding between this model and another.
  509. *
  510. * @see lithium\data\model\Relationship
  511. * @param string $type The type of relationship to create. Must be one of `'hasOne'`,
  512. * `'hasMany'` or `'belongsTo'`.
  513. * @param string $name The name of the relationship. If this is also the name of the model,
  514. * the model must be in the same namespace as this model. Otherwise, the
  515. * fully-namespaced path to the model class must be specified in `$config`.
  516. * @param array $config Any other configuration that should be specified in the relationship.
  517. * See the `Relationship` class for more information.
  518. * @return object Returns an instance of the `Relationship` class that defines the connection.
  519. */
  520. public static function bind($type, $name, array $config = array()) {
  521. $self = static::_object();
  522. if (!isset($self->_relationTypes[$type])) {
  523. throw new ConfigException("Invalid relationship type '{$type}' specified.");
  524. }
  525. $rel = static::_connection()->relationship(get_called_class(), $type, $name, $config);
  526. return static::_object()->_relations[$name] = $rel;
  527. }
  528. /**
  529. * Lazy-initialize the schema for this Model object, if it is not already manually set in the
  530. * object. You can declare `protected $_schema = array(...)` to define the schema manually.
  531. *
  532. * @param string $field Optional. You may pass a field name to get schema information for just
  533. * one field. Otherwise, an array containing all fields is returned.
  534. * @return array
  535. */
  536. public static function schema($field = null) {
  537. $self = static::_object();
  538. if (!$self->_schema) {
  539. $self->_schema = static::_connection()->describe($self::meta('source'), $self->_meta);
  540. }
  541. if (is_string($field) && $field) {
  542. return isset($self->_schema[$field]) ? $self->_schema[$field] : null;
  543. }
  544. return $self->_schema;
  545. }
  546. /**
  547. * Checks to see if a particular field exists in a model's schema. Can check a single field, or
  548. * return the first field found in an array of multiple options.
  549. *
  550. * @param mixed $field A single field (string) or list of fields (array) to check the existence
  551. * of.
  552. * @return mixed If `$field` is a string, returns a boolean indicating whether or not that field
  553. * exists. If `$field` is an array, returns the first field found, or `false` if none of
  554. * the fields in the list are found.
  555. */
  556. public static function hasField($field) {
  557. if (is_array($field)) {
  558. foreach ($field as $f) {
  559. if (static::hasField($f)) {
  560. return $f;
  561. }
  562. }
  563. return false;
  564. }
  565. $schema = static::schema();
  566. return ($schema && isset($schema[$field]));
  567. }
  568. /**
  569. * Instantiates a new record object, initialized with any data passed in. For example:
  570. * {{{
  571. * $post = Post::create(array("title" => "New post"));
  572. * echo $post->title; // echoes "New post"
  573. * $success = $post->save();
  574. * }}}
  575. *
  576. * @param array $data Any data that this record should be populated with initially.
  577. * @param array $options Options to be passed to item.
  578. * @return object Returns a new, **un-saved** record object.
  579. */
  580. public static function create(array $data = array(), array $options = array()) {
  581. $self = static::_object();
  582. $classes = $self->_classes;
  583. $params = compact('data', 'options');
  584. return static::_filter(__FUNCTION__, $params, function($self, $params) use ($classes) {
  585. $data = $params['data'];
  586. $options = $params['options'];
  587. if ($schema = $self::schema()) {
  588. foreach ($schema as $field => $config) {
  589. if (!isset($data[$field]) && isset($config['default'])) {
  590. $data[$field] = $config['default'];
  591. }
  592. }
  593. }
  594. if ($self::meta('connection')) {
  595. return $self::invokeMethod('_connection')->item($self, $data, $options);
  596. }
  597. return new $classes['entity'](array('model' => $self) + compact('data') + $options);
  598. });
  599. }
  600. /**
  601. * An instance method (called on record and document objects) to create or update the record or
  602. * document in the database that corresponds to `$entity`. For example:
  603. * {{{
  604. * $post = Post::create();
  605. * $post->title = "My post";
  606. * $post->save(null, array('validate' => false));
  607. * }}}
  608. *
  609. * @param object $entity The record or document object to be saved in the database. This
  610. * parameter is implicit and should not be passed under normal circumstances.
  611. * In the above example, the call to `save()` on the `$post` object is
  612. * transparently proxied through to the `Post` model class, and `$post` is passed
  613. * in as the `$entity` parameter.
  614. * @param array $data Any data that should be assigned to the record before it is saved.
  615. * @param array $options Options:
  616. * - `'callbacks'` _boolean_: If `false`, all callbacks will be disabled before
  617. * executing. Defaults to `true`.
  618. * - `'validate'` _boolean_: If `false`, validation will be skipped, and the record will
  619. * be immediately saved. Defaults to `true`.
  620. * - `'whitelist'` _array_: An array of fields that are allowed to be saved to this
  621. * record.
  622. *
  623. * @return boolean Returns `true` on a successful save operation, `false` on failure.
  624. */
  625. public function save($entity, $data = null, array $options = array()) {
  626. $self = static::_object();
  627. $classes = $self->_classes;
  628. $_meta = array('model' => get_called_class()) + $self->_meta;
  629. $_schema = $self->_schema;
  630. $defaults = array(
  631. 'validate' => true,
  632. 'whitelist' => null,
  633. 'callbacks' => true,
  634. 'locked' => $self->_meta['locked'],
  635. );
  636. $options += $defaults + compact('classes');
  637. $params = compact('entity', 'data', 'options');
  638. $filter = function($self, $params) use ($_meta, $_schema) {
  639. $entity = $params['entity'];
  640. $options = $params['options'];
  641. $class = $options['classes']['query'];
  642. if ($params['data']) {
  643. $entity->set($params['data']);
  644. }
  645. if ($rules = $options['validate']) {
  646. if (!$entity->validates(is_array($rules) ? compact('rules') : array())) {
  647. return false;
  648. }
  649. }
  650. if ($options['whitelist'] || $options['locked']) {
  651. $whitelist = $options['whitelist'] ?: array_keys($_schema);
  652. }
  653. $type = $entity->exists() ? 'update' : 'create';
  654. $query = new $class(compact('type', 'whitelist', 'entity') + $options + $_meta);
  655. return $self::invokeMethod('_connection')->{$type}($query, $options);
  656. };
  657. if (!$options['callbacks']) {
  658. return $filter($entity, $options);
  659. }
  660. return static::_filter(__FUNCTION__, $params, $filter);
  661. }
  662. /**
  663. * Indicates whether the `Model`'s current data validates, given the
  664. * current rules setup.
  665. *
  666. * @param string $entity Model record to validate.
  667. * @param array $options Options.
  668. * @return boolean Success.
  669. */
  670. public function validates($entity, array $options = array()) {
  671. $defaults = array('rules' => $this->validates);
  672. $options += $defaults;
  673. $self = static::_object();
  674. $validator = $self->_classes['validator'];
  675. $params = compact('entity', 'options');
  676. $filter = function($parent, $params) use (&$self, $validator) {
  677. extract($params);
  678. $rules = $options['rules'];
  679. unset ($options['rules']);
  680. if ($errors = $validator::check($entity->data(), $rules, $options)) {
  681. $entity->errors($errors);
  682. }
  683. return empty($errors);
  684. };
  685. return static::_filter(__FUNCTION__, $params, $filter);
  686. }
  687. /**
  688. * Deletes the data associated with the current `Model`.
  689. *
  690. * @param string $entity Entity to delete.
  691. * @param array $options Options.
  692. * @return boolean Success.
  693. */
  694. public function delete($entity, array $options = array()) {
  695. $self = static::_object();
  696. $_class = $self->_classes['query'];
  697. $params = compact('entity', 'options');
  698. return static::_filter(__FUNCTION__, $params, function($self, $params) use ($_class) {
  699. $type = 'delete';
  700. $entity = $params['entity'];
  701. $options = $params['options'] + array('model' => $self) + compact('type', 'entity');
  702. return $self::invokeMethod('_connection')->delete(new $_class($options), $options);
  703. });
  704. }
  705. /**
  706. * Update multiple records or documents with the given data, restricted by the given set of
  707. * criteria (optional).
  708. *
  709. * @param mixed $data Typically an array of key/value pairs that specify the new data with which
  710. * the records will be updated. For SQL databases, this can optionally be an SQL
  711. * fragment representing the `SET` clause of an `UPDATE` query.
  712. * @param mixed $conditions An array of key/value pairs representing the scope of the records
  713. * to be updated.
  714. * @param array $options Any database-specific options to use when performing the operation. See
  715. * the `delete()` method of the corresponding backend database for available
  716. * options.
  717. * @return boolean Returns `true` if the update operation succeeded, otherwise `false`.
  718. */
  719. public static function update($data, $conditions = array(), array $options = array()) {
  720. $self = static::_object();
  721. $_class = $self->_classes['query'];
  722. $params = compact('data', 'conditions', 'options');
  723. return static::_filter(__FUNCTION__, $params, function($self, $params) use ($_class) {
  724. $options = $params + $params['options'] + array('model' => $self, 'type' => 'update');
  725. unset($options['options']);
  726. return $self::invokeMethod('_connection')->update(new $_class($options), $options);
  727. });
  728. }
  729. /**
  730. * Remove multiple documents or records based on a given set of criteria. **WARNING**: If no
  731. * criteria are specified, or if the criteria (`$conditions`) is an empty value (i.e. an empty
  732. * array or `null`), all the data in the backend data source (i.e. table or collection) _will_
  733. * be deleted.
  734. *
  735. * @param mixed $conditions An array of key/value pairs representing the scope of the records or
  736. * documents to be deleted.
  737. * @param array $options Any database-specific options to use when performing the operation. See
  738. * the `delete()` method of the corresponding backend database for available
  739. * options.
  740. * @return boolean Returns `true` if the remove operation succeeded, otherwise `false`.
  741. */
  742. public static function remove($conditions = array(), array $options = array()) {
  743. $self = static::_object();
  744. $_class = $self->_classes['query'];
  745. $params = compact('conditions', 'options');
  746. return static::_filter(__FUNCTION__, $params, function($self, $params) use ($_class) {
  747. $options = $params['options'] + $params + array('model' => $self, 'type' => 'delete');
  748. unset($options['options']);
  749. return $self::invokeMethod('_connection')->delete(new $_class($options), $options);
  750. });
  751. }
  752. /**
  753. * Gets just the class name portion of a fully-name-spaced class name, i.e.
  754. * `app\models\Post::_name()` returns `'Post'`.
  755. *
  756. * @return string
  757. */
  758. protected static function _name() {
  759. static $name;
  760. return $name ?: $name = join('', array_slice(explode("\\", get_called_class()), -1));
  761. }
  762. /**
  763. * Gets the connection object to which this model is bound. Throws exceptions if a connection
  764. * isn't set, or if the connection named isn't configured.
  765. *
  766. * @return object Returns an instance of `lithium\data\Source` from the connection configuration
  767. * to which this model is bound.
  768. */
  769. protected static function &_connection() {
  770. $self = static::_object();
  771. $connections = $self->_classes['connections'];
  772. $name = isset($self->_meta['connection']) ? $self->_meta['connection'] : null;
  773. if (!$name) {
  774. throw new ConfigException("Connection name not defined");
  775. }
  776. if ($conn = $connections::get($name)) {
  777. return $conn;
  778. }
  779. throw new ConfigException("The data connection {$name} is not configured");
  780. }
  781. /**
  782. * Wraps `StaticObject::applyFilter()` to account for object instances.
  783. *
  784. * @see lithium\core\StaticObject::applyFilter()
  785. * @param string $method
  786. * @param mixed $closure
  787. */
  788. public static function applyFilter($method, $closure = null) {
  789. $instance = static::_object();
  790. $methods = (array) $method;
  791. foreach ($methods as $method) {
  792. if (!isset($instance->_instanceFilters[$method])) {
  793. $instance->_instanceFilters[$method] = array();
  794. }
  795. $instance->_instanceFilters[$method][] = $closure;
  796. }
  797. }
  798. /**
  799. * Wraps `StaticObject::_filter()` to account for object instances.
  800. *
  801. * @see lithium\core\StaticObject::_filter()
  802. * @param string $method
  803. * @param array $params
  804. * @param mixed $callback
  805. * @param array $filters Defaults to empty array.
  806. * @return object
  807. */
  808. protected static function _filter($method, $params, $callback, $filters = array()) {
  809. if (!strpos($method, '::')) {
  810. $method = get_called_class() . '::' . $method;
  811. }
  812. list($class, $method) = explode('::', $method, 2);
  813. $instance = static::_object();
  814. if (isset($instance->_instanceFilters[$method])) {
  815. $filters = array_merge($instance->_instanceFilters[$method], $filters);
  816. }
  817. return parent::_filter($method, $params, $callback, $filters);
  818. }
  819. protected static function &_object() {
  820. $class = get_called_class();
  821. if (!isset(static::$_instances[$class])) {
  822. static::$_instances[$class] = new $class();
  823. }
  824. return static::$_instances[$class];
  825. }
  826. /**
  827. * Iterates through relationship types to construct relation map.
  828. *
  829. * @return void
  830. * @todo See if this can be rewritten to be lazy.
  831. */
  832. protected static function _relations() {
  833. $self = static::_object();
  834. if (!$self->_meta['connection']) {
  835. return;
  836. }
  837. foreach ($self->_relationTypes as $type => $keys) {
  838. foreach (Set::normalize($self->{$type}) as $name => $config) {
  839. static::bind($type, $name, (array) $config);
  840. }
  841. }
  842. }
  843. /**
  844. * Helper function for setting/getting base class settings.
  845. *
  846. * @param string $class Classname.
  847. * @param boolean $set If `true`, then the `$class` will be set.
  848. * @return boolean Success.
  849. */
  850. protected static function _isBase($class = null, $set = false) {
  851. if ($set) {
  852. static::$_baseClasses[$class] = true;
  853. }
  854. return isset(static::$_baseClasses[$class]);
  855. }
  856. /**
  857. * Exports an array of custom finders which use the filter system to wrap around `find()`.
  858. *
  859. * @return void
  860. */
  861. protected static function _findFilters() {
  862. $self = static::_object();
  863. $_query =& $self->_query;
  864. return array(
  865. 'first' => function($self, $params, $chain) {
  866. $params['options']['limit'] = 1;
  867. $data = $chain->next($self, $params, $chain);
  868. $data = is_object($data) ? $data->rewind() : $data;
  869. return $data ?: null;
  870. },
  871. 'list' => function($self, $params, $chain) {
  872. $result = array();
  873. $meta = $self::meta();
  874. $name = $meta['key'];
  875. foreach ($chain->next($self, $params, $chain) as $entity) {
  876. $key = $entity->{$name};
  877. $result[is_scalar($key) ? $key : (string) $key] = $entity->{$meta['title']};
  878. }
  879. return $result;
  880. },
  881. 'count' => function($self, $params, $chain) use ($_query) {
  882. $model = $self;
  883. $type = $params['type'];
  884. $options = array_diff_key($params['options'], $_query);
  885. $classes = $options['classes'];
  886. unset($options['classes']);
  887. if ($options && !isset($params['options']['conditions'])) {
  888. $options = array('conditions' => $options);
  889. } else {
  890. $options = $params['options'];
  891. }
  892. $options += compact('classes', 'model');
  893. $query = new $classes['query'](array('type' => 'read') + $options);
  894. return $self::invokeMethod('_connection')->calculation('count', $query, $options);
  895. }
  896. );
  897. }
  898. }
  899. ?>