Model.php 46 KB

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