vendor/contao/core-bundle/src/Resources/contao/library/Contao/Model.php line 1292

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Contao.
  4. *
  5. * (c) Leo Feyer
  6. *
  7. * @license LGPL-3.0-or-later
  8. */
  9. namespace Contao;
  10. use Contao\Database\Result;
  11. use Contao\Database\Statement;
  12. use Contao\Model\Collection;
  13. use Contao\Model\QueryBuilder;
  14. use Contao\Model\Registry;
  15. /**
  16. * Reads objects from and writes them to the database
  17. *
  18. * The class allows you to find and automatically join database records and to
  19. * convert the result into objects. It also supports creating new objects and
  20. * persisting them in the database.
  21. *
  22. * Usage:
  23. *
  24. * // Write
  25. * $user = new UserModel();
  26. * $user->name = 'Leo Feyer';
  27. * $user->city = 'Wuppertal';
  28. * $user->save();
  29. *
  30. * // Read
  31. * $user = UserModel::findByCity('Wuppertal');
  32. *
  33. * while ($user->next())
  34. * {
  35. * echo $user->name;
  36. * }
  37. *
  38. * @property integer $id The ID
  39. * @property string $customTpl A custom template
  40. */
  41. abstract class Model
  42. {
  43. /**
  44. * Insert flag
  45. */
  46. const INSERT = 1;
  47. /**
  48. * Update flag
  49. */
  50. const UPDATE = 2;
  51. /**
  52. * Table name
  53. * @var string
  54. */
  55. protected static $strTable;
  56. /**
  57. * Primary key
  58. * @var string
  59. */
  60. protected static $strPk = 'id';
  61. /**
  62. * Class name cache
  63. * @var array
  64. */
  65. protected static $arrClassNames = array();
  66. /**
  67. * Data
  68. * @var array
  69. */
  70. protected $arrData = array();
  71. /**
  72. * Modified keys
  73. * @var array
  74. */
  75. protected $arrModified = array();
  76. /**
  77. * Relations
  78. * @var array
  79. */
  80. protected $arrRelations = array();
  81. /**
  82. * Related
  83. * @var array
  84. */
  85. protected $arrRelated = array();
  86. /**
  87. * Prevent saving
  88. * @var boolean
  89. */
  90. protected $blnPreventSaving = false;
  91. /**
  92. * Load the relations and optionally process a result set
  93. *
  94. * @param Result|array $objResult An optional database result or array
  95. */
  96. public function __construct($objResult=null)
  97. {
  98. $this->arrModified = array();
  99. $objDca = DcaExtractor::getInstance(static::$strTable);
  100. $this->arrRelations = $objDca->getRelations();
  101. if ($objResult !== null)
  102. {
  103. $arrRelated = array();
  104. if ($objResult instanceof Result)
  105. {
  106. $arrData = $objResult->row();
  107. }
  108. else
  109. {
  110. $arrData = (array) $objResult;
  111. }
  112. // Look for joined fields
  113. foreach ($arrData as $k=>$v)
  114. {
  115. if (static::isJoinedField($k))
  116. {
  117. list($key, $field) = explode('__', $k, 2);
  118. if (!isset($arrRelated[$key]))
  119. {
  120. $arrRelated[$key] = array();
  121. }
  122. $arrRelated[$key][$field] = $v;
  123. unset($arrData[$k]);
  124. }
  125. }
  126. $objRegistry = Registry::getInstance();
  127. $this->setRow($arrData); // see #5439
  128. $objRegistry->register($this);
  129. // Create the related models
  130. foreach ($arrRelated as $key=>$row)
  131. {
  132. if (!isset($this->arrRelations[$key]['table']))
  133. {
  134. throw new \Exception('Incomplete relation defined for ' . static::$strTable . '.' . $key);
  135. }
  136. $table = $this->arrRelations[$key]['table'];
  137. /** @var static $strClass */
  138. $strClass = static::getClassFromTable($table);
  139. $intPk = $strClass::getPk();
  140. // If the primary key is empty, set null (see #5356)
  141. if (!isset($row[$intPk]))
  142. {
  143. $this->arrRelated[$key] = null;
  144. }
  145. else
  146. {
  147. $objRelated = $objRegistry->fetch($table, $row[$intPk]);
  148. if ($objRelated !== null)
  149. {
  150. $objRelated->mergeRow($row);
  151. }
  152. else
  153. {
  154. /** @var static $objRelated */
  155. $objRelated = new $strClass();
  156. $objRelated->setRow($row);
  157. $objRegistry->register($objRelated);
  158. }
  159. $this->arrRelated[$key] = $objRelated;
  160. }
  161. }
  162. }
  163. }
  164. /**
  165. * Unset the primary key when cloning an object
  166. */
  167. public function __clone()
  168. {
  169. $this->arrModified = array();
  170. $this->blnPreventSaving = false;
  171. unset($this->arrData[static::$strPk]);
  172. }
  173. /**
  174. * Clone a model with its original values
  175. *
  176. * @return static The model
  177. */
  178. public function cloneOriginal()
  179. {
  180. $clone = clone $this;
  181. $clone->setRow($this->originalRow());
  182. return $clone;
  183. }
  184. /**
  185. * Set an object property
  186. *
  187. * @param string $strKey The property name
  188. * @param mixed $varValue The property value
  189. */
  190. public function __set($strKey, $varValue)
  191. {
  192. if (isset($this->arrData[$strKey]) && $this->arrData[$strKey] === $varValue)
  193. {
  194. return;
  195. }
  196. $this->markModified($strKey);
  197. $this->arrData[$strKey] = $varValue;
  198. unset($this->arrRelated[$strKey]);
  199. }
  200. /**
  201. * Return an object property
  202. *
  203. * @param string $strKey The property key
  204. *
  205. * @return mixed|null The property value or null
  206. */
  207. public function __get($strKey)
  208. {
  209. return $this->arrData[$strKey] ?? null;
  210. }
  211. /**
  212. * Check whether a property is set
  213. *
  214. * @param string $strKey The property key
  215. *
  216. * @return boolean True if the property is set
  217. */
  218. public function __isset($strKey)
  219. {
  220. return isset($this->arrData[$strKey]);
  221. }
  222. /**
  223. * Return the name of the primary key
  224. *
  225. * @return string The primary key
  226. */
  227. public static function getPk()
  228. {
  229. return static::$strPk;
  230. }
  231. /**
  232. * Return an array of unique field/column names (without the PK)
  233. *
  234. * @return array
  235. */
  236. public static function getUniqueFields()
  237. {
  238. $objDca = DcaExtractor::getInstance(static::getTable());
  239. return $objDca->getUniqueFields();
  240. }
  241. /**
  242. * Return the name of the related table
  243. *
  244. * @return string The table name
  245. */
  246. public static function getTable()
  247. {
  248. return static::$strTable;
  249. }
  250. /**
  251. * Return the current record as associative array
  252. *
  253. * @return array The data record
  254. */
  255. public function row()
  256. {
  257. return $this->arrData;
  258. }
  259. /**
  260. * Return the original values as associative array
  261. *
  262. * @return array The original data
  263. */
  264. public function originalRow()
  265. {
  266. $row = $this->row();
  267. if (!$this->isModified())
  268. {
  269. return $row;
  270. }
  271. $originalRow = array();
  272. foreach ($row as $k=>$v)
  273. {
  274. $originalRow[$k] = $this->arrModified[$k] ?? $v;
  275. }
  276. return $originalRow;
  277. }
  278. /**
  279. * Return true if the model has been modified
  280. *
  281. * @return boolean True if the model has been modified
  282. */
  283. public function isModified()
  284. {
  285. return !empty($this->arrModified);
  286. }
  287. /**
  288. * Set the current record from an array
  289. *
  290. * @param array $arrData The data record
  291. *
  292. * @return static The model object
  293. */
  294. public function setRow(array $arrData)
  295. {
  296. foreach ($arrData as $k=>$v)
  297. {
  298. if (static::isJoinedField($k))
  299. {
  300. unset($arrData[$k]);
  301. }
  302. }
  303. $this->arrData = $arrData;
  304. return $this;
  305. }
  306. /**
  307. * Set the current record from an array preserving modified but unsaved fields
  308. *
  309. * @param array $arrData The data record
  310. *
  311. * @return static The model object
  312. */
  313. public function mergeRow(array $arrData)
  314. {
  315. foreach ($arrData as $k=>$v)
  316. {
  317. if (static::isJoinedField($k))
  318. {
  319. continue;
  320. }
  321. if (!isset($this->arrModified[$k]))
  322. {
  323. $this->arrData[$k] = $v;
  324. }
  325. }
  326. return $this;
  327. }
  328. /**
  329. * Mark a field as modified
  330. *
  331. * @param string $strKey The field key
  332. */
  333. public function markModified($strKey)
  334. {
  335. if (!isset($this->arrModified[$strKey]))
  336. {
  337. $this->arrModified[$strKey] = $this->arrData[$strKey] ?? null;
  338. }
  339. }
  340. /**
  341. * Return the object instance
  342. *
  343. * @return static The model object
  344. */
  345. public function current()
  346. {
  347. return $this;
  348. }
  349. /**
  350. * Save the current record
  351. *
  352. * @return static The model object
  353. *
  354. * @throws \InvalidArgumentException If an argument is passed
  355. * @throws \RuntimeException If the model cannot be saved
  356. */
  357. public function save()
  358. {
  359. // Deprecated call
  360. if (\func_num_args() > 0)
  361. {
  362. throw new \InvalidArgumentException('The $blnForceInsert argument has been removed (see system/docs/UPGRADE.md)');
  363. }
  364. // The instance cannot be saved
  365. if ($this->blnPreventSaving)
  366. {
  367. throw new \RuntimeException('The model instance has been detached and cannot be saved');
  368. }
  369. $objDatabase = Database::getInstance();
  370. $arrFields = $objDatabase->getFieldNames(static::$strTable);
  371. // The model is in the registry
  372. if (Registry::getInstance()->isRegistered($this))
  373. {
  374. $arrSet = array();
  375. $arrRow = $this->row();
  376. // Only update modified fields
  377. foreach ($this->arrModified as $k=>$v)
  378. {
  379. // Only set fields that exist in the DB
  380. if (\in_array($k, $arrFields))
  381. {
  382. $arrSet[$k] = $arrRow[$k];
  383. }
  384. }
  385. $arrSet = $this->preSave($arrSet);
  386. // No modified fields
  387. if (empty($arrSet))
  388. {
  389. return $this;
  390. }
  391. // Track primary key changes
  392. $intPk = $this->arrModified[static::$strPk] ?? $this->{static::$strPk};
  393. if ($intPk === null)
  394. {
  395. throw new \RuntimeException('The primary key has not been set');
  396. }
  397. // Update the row
  398. $objDatabase->prepare("UPDATE " . static::$strTable . " %s WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
  399. ->set($arrSet)
  400. ->execute($intPk);
  401. $this->postSave(self::UPDATE);
  402. $this->arrModified = array(); // reset after postSave()
  403. }
  404. // The model is not yet in the registry
  405. else
  406. {
  407. $arrSet = $this->row();
  408. // Remove fields that do not exist in the DB
  409. foreach ($arrSet as $k=>$v)
  410. {
  411. if (!\in_array($k, $arrFields))
  412. {
  413. unset($arrSet[$k]);
  414. }
  415. }
  416. $arrSet = $this->preSave($arrSet);
  417. // No modified fields
  418. if (empty($arrSet))
  419. {
  420. return $this;
  421. }
  422. // Insert a new row
  423. $stmt = $objDatabase->prepare("INSERT INTO " . static::$strTable . " %s")
  424. ->set($arrSet)
  425. ->execute();
  426. if (static::$strPk == 'id')
  427. {
  428. $this->id = $stmt->insertId;
  429. }
  430. $this->postSave(self::INSERT);
  431. $this->arrModified = array(); // reset after postSave()
  432. Registry::getInstance()->register($this);
  433. }
  434. return $this;
  435. }
  436. /**
  437. * Modify the current row before it is stored in the database
  438. *
  439. * @param array $arrSet The data array
  440. *
  441. * @return array The modified data array
  442. */
  443. protected function preSave(array $arrSet)
  444. {
  445. return $arrSet;
  446. }
  447. /**
  448. * Modify the current row after it has been stored in the database
  449. *
  450. * @param integer $intType The query type (Model::INSERT or Model::UPDATE)
  451. */
  452. protected function postSave($intType)
  453. {
  454. if ($intType == self::INSERT)
  455. {
  456. $this->refresh(); // might have been modified by default values or triggers
  457. }
  458. }
  459. /**
  460. * Delete the current record and return the number of affected rows
  461. *
  462. * @return integer The number of affected rows
  463. */
  464. public function delete()
  465. {
  466. // Track primary key changes
  467. $intPk = $this->arrModified[static::$strPk] ?? $this->{static::$strPk};
  468. // Delete the row
  469. $intAffected = Database::getInstance()->prepare("DELETE FROM " . static::$strTable . " WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
  470. ->execute($intPk)
  471. ->affectedRows;
  472. if ($intAffected)
  473. {
  474. // Unregister the model
  475. Registry::getInstance()->unregister($this);
  476. // Remove the primary key (see #6162)
  477. $this->arrData[static::$strPk] = null;
  478. }
  479. return $intAffected;
  480. }
  481. /**
  482. * Lazy load related records
  483. *
  484. * @param string $strKey The property name
  485. * @param array $arrOptions An optional options array
  486. *
  487. * @return static|Collection|null The model or a model collection if there are multiple rows
  488. *
  489. * @throws \Exception If $strKey is not a related field
  490. */
  491. public function getRelated($strKey, array $arrOptions=array())
  492. {
  493. // The related model has been loaded before
  494. if (\array_key_exists($strKey, $this->arrRelated))
  495. {
  496. return $this->arrRelated[$strKey];
  497. }
  498. // The relation does not exist
  499. if (!isset($this->arrRelations[$strKey]))
  500. {
  501. $table = static::getTable();
  502. throw new \Exception("Field $table.$strKey does not seem to be related");
  503. }
  504. // The relation exists but there is no reference yet (see #6161 and #458)
  505. if (empty($this->$strKey))
  506. {
  507. return null;
  508. }
  509. $arrRelation = $this->arrRelations[$strKey];
  510. /** @var static $strClass */
  511. $strClass = static::getClassFromTable($arrRelation['table']);
  512. // Load the related record(s)
  513. if ($arrRelation['type'] == 'hasOne' || $arrRelation['type'] == 'belongsTo')
  514. {
  515. $this->arrRelated[$strKey] = $strClass::findOneBy($arrRelation['field'], $this->$strKey, $arrOptions);
  516. }
  517. elseif ($arrRelation['type'] == 'hasMany' || $arrRelation['type'] == 'belongsToMany')
  518. {
  519. if (isset($arrRelation['delimiter']))
  520. {
  521. $arrValues = StringUtil::trimsplit($arrRelation['delimiter'], $this->$strKey);
  522. }
  523. else
  524. {
  525. $arrValues = StringUtil::deserialize($this->$strKey, true);
  526. }
  527. $objModel = null;
  528. if (\is_array($arrValues))
  529. {
  530. // Handle UUIDs (see #6525 and #8850)
  531. if ($arrRelation['table'] == 'tl_files' && $arrRelation['field'] == 'uuid')
  532. {
  533. /** @var FilesModel $strClass */
  534. $objModel = $strClass::findMultipleByUuids($arrValues, $arrOptions);
  535. }
  536. else
  537. {
  538. $strField = $arrRelation['table'] . '.' . Database::quoteIdentifier($arrRelation['field']);
  539. $arrOptions = array_merge
  540. (
  541. array
  542. (
  543. 'order' => Database::getInstance()->findInSet($strField, $arrValues)
  544. ),
  545. $arrOptions
  546. );
  547. $objModel = $strClass::findBy(array($strField . " IN('" . implode("','", $arrValues) . "')"), null, $arrOptions);
  548. }
  549. }
  550. $this->arrRelated[$strKey] = $objModel;
  551. }
  552. return $this->arrRelated[$strKey];
  553. }
  554. /**
  555. * Reload the data from the database discarding all modifications
  556. */
  557. public function refresh()
  558. {
  559. // Track primary key changes
  560. $intPk = $this->arrModified[static::$strPk] ?? $this->{static::$strPk};
  561. // Reload the database record
  562. $res = Database::getInstance()->prepare("SELECT * FROM " . static::$strTable . " WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
  563. ->execute($intPk);
  564. $this->setRow($res->row());
  565. }
  566. /**
  567. * Detach the model from the registry
  568. *
  569. * @param boolean $blnKeepClone Keeps a clone of the model in the registry
  570. */
  571. public function detach($blnKeepClone=true)
  572. {
  573. $registry = Registry::getInstance();
  574. if (!$registry->isRegistered($this))
  575. {
  576. return;
  577. }
  578. $registry->unregister($this);
  579. if ($blnKeepClone)
  580. {
  581. $this->cloneOriginal()->attach();
  582. }
  583. }
  584. /**
  585. * Attach the model to the registry
  586. */
  587. public function attach()
  588. {
  589. Registry::getInstance()->register($this);
  590. }
  591. /**
  592. * Called when the model is attached to the model registry
  593. *
  594. * @param Registry $registry The model registry
  595. */
  596. public function onRegister(Registry $registry)
  597. {
  598. // Register aliases to unique fields
  599. foreach (static::getUniqueFields() as $strColumn)
  600. {
  601. $varAliasValue = $this->{$strColumn};
  602. if (!$registry->isRegisteredAlias($this, $strColumn, $varAliasValue))
  603. {
  604. $registry->registerAlias($this, $strColumn, $varAliasValue);
  605. }
  606. }
  607. }
  608. /**
  609. * Called when the model is detached from the model registry
  610. *
  611. * @param Registry $registry The model registry
  612. */
  613. public function onUnregister(Registry $registry)
  614. {
  615. // Unregister aliases to unique fields
  616. foreach (static::getUniqueFields() as $strColumn)
  617. {
  618. $varAliasValue = $this->{$strColumn};
  619. if ($registry->isRegisteredAlias($this, $strColumn, $varAliasValue))
  620. {
  621. $registry->unregisterAlias($this, $strColumn, $varAliasValue);
  622. }
  623. }
  624. }
  625. /**
  626. * Prevent saving the model
  627. *
  628. * @param boolean $blnKeepClone Keeps a clone of the model in the registry
  629. */
  630. public function preventSaving($blnKeepClone=true)
  631. {
  632. $this->detach($blnKeepClone);
  633. $this->blnPreventSaving = true;
  634. }
  635. /**
  636. * Find a single record by its primary key
  637. *
  638. * @param mixed $varValue The property value
  639. * @param array $arrOptions An optional options array
  640. *
  641. * @return static The model or null if the result is empty
  642. */
  643. public static function findByPk($varValue, array $arrOptions=array())
  644. {
  645. if ($varValue === null)
  646. {
  647. trigger_deprecation('contao/core-bundle', '4.13', 'Passing "null" as primary key has been deprecated and will no longer work in Contao 5.0.', __CLASS__);
  648. return null;
  649. }
  650. // Try to load from the registry
  651. if (empty($arrOptions))
  652. {
  653. $objModel = Registry::getInstance()->fetch(static::$strTable, $varValue);
  654. if ($objModel !== null)
  655. {
  656. return $objModel;
  657. }
  658. }
  659. $arrOptions = array_merge
  660. (
  661. array
  662. (
  663. 'limit' => 1,
  664. 'column' => static::$strPk,
  665. 'value' => $varValue,
  666. 'return' => 'Model'
  667. ),
  668. $arrOptions
  669. );
  670. return static::find($arrOptions);
  671. }
  672. /**
  673. * Find a single record by its ID or alias
  674. *
  675. * @param mixed $varId The ID or alias
  676. * @param array $arrOptions An optional options array
  677. *
  678. * @return static The model or null if the result is empty
  679. */
  680. public static function findByIdOrAlias($varId, array $arrOptions=array())
  681. {
  682. $isAlias = !preg_match('/^[1-9]\d*$/', $varId);
  683. // Try to load from the registry
  684. if (!$isAlias && empty($arrOptions))
  685. {
  686. $objModel = Registry::getInstance()->fetch(static::$strTable, $varId);
  687. if ($objModel !== null)
  688. {
  689. return $objModel;
  690. }
  691. }
  692. $t = static::$strTable;
  693. $arrOptions = array_merge
  694. (
  695. array
  696. (
  697. 'limit' => 1,
  698. 'column' => $isAlias ? array("BINARY $t.alias=?") : array("$t.id=?"),
  699. 'value' => $varId,
  700. 'return' => 'Model'
  701. ),
  702. $arrOptions
  703. );
  704. return static::find($arrOptions);
  705. }
  706. /**
  707. * Find multiple records by their IDs
  708. *
  709. * @param array $arrIds An array of IDs
  710. * @param array $arrOptions An optional options array
  711. *
  712. * @return Collection|null The model collection or null if there are no records
  713. */
  714. public static function findMultipleByIds($arrIds, array $arrOptions=array())
  715. {
  716. if (empty($arrIds) || !\is_array($arrIds))
  717. {
  718. return null;
  719. }
  720. $arrRegistered = array();
  721. $arrUnregistered = array();
  722. // Search for registered models
  723. foreach ($arrIds as $intId)
  724. {
  725. if (empty($arrOptions))
  726. {
  727. $arrRegistered[$intId] = Registry::getInstance()->fetch(static::$strTable, $intId);
  728. }
  729. if (!isset($arrRegistered[$intId]))
  730. {
  731. $arrUnregistered[] = $intId;
  732. }
  733. }
  734. // Fetch only the missing models from the database
  735. if (!empty($arrUnregistered))
  736. {
  737. $t = static::$strTable;
  738. $arrOptions = array_merge
  739. (
  740. array
  741. (
  742. 'column' => array("$t.id IN(" . implode(',', array_map('\intval', $arrUnregistered)) . ")"),
  743. 'order' => Database::getInstance()->findInSet("$t.id", $arrIds),
  744. 'return' => 'Collection'
  745. ),
  746. $arrOptions
  747. );
  748. $objMissing = static::find($arrOptions);
  749. if ($objMissing !== null)
  750. {
  751. foreach ($objMissing as $objCurrent)
  752. {
  753. $intId = $objCurrent->{static::$strPk};
  754. $arrRegistered[$intId] = $objCurrent;
  755. }
  756. }
  757. }
  758. $arrRegistered = array_filter(array_values($arrRegistered));
  759. if (empty($arrRegistered))
  760. {
  761. return null;
  762. }
  763. return static::createCollection($arrRegistered, static::$strTable);
  764. }
  765. /**
  766. * Find a single record by various criteria
  767. *
  768. * @param mixed $strColumn The property name
  769. * @param mixed $varValue The property value
  770. * @param array $arrOptions An optional options array
  771. *
  772. * @return static The model or null if the result is empty
  773. */
  774. public static function findOneBy($strColumn, $varValue, array $arrOptions=array())
  775. {
  776. $arrOptions = array_merge
  777. (
  778. array
  779. (
  780. 'limit' => 1,
  781. 'column' => $strColumn,
  782. 'value' => $varValue,
  783. 'return' => 'Model'
  784. ),
  785. $arrOptions
  786. );
  787. return static::find($arrOptions);
  788. }
  789. /**
  790. * Find records by various criteria
  791. *
  792. * @param mixed $strColumn The property name
  793. * @param mixed $varValue The property value
  794. * @param array $arrOptions An optional options array
  795. *
  796. * @return static|Collection|null A model, model collection or null if the result is empty
  797. */
  798. public static function findBy($strColumn, $varValue, array $arrOptions=array())
  799. {
  800. $blnModel = false;
  801. $arrColumn = (array) $strColumn;
  802. if (\count($arrColumn) == 1 && ($arrColumn[0] === static::getPk() || \in_array($arrColumn[0], static::getUniqueFields())))
  803. {
  804. $blnModel = true;
  805. if ($varValue === null && $arrColumn[0] === static::getPk())
  806. {
  807. trigger_deprecation('contao/core-bundle', '4.13', 'Passing "null" as primary key has been deprecated and will no longer work in Contao 5.0.', __CLASS__);
  808. return null;
  809. }
  810. }
  811. $arrOptions = array_merge
  812. (
  813. array
  814. (
  815. 'column' => $strColumn,
  816. 'value' => $varValue,
  817. 'return' => $blnModel ? 'Model' : 'Collection'
  818. ),
  819. $arrOptions
  820. );
  821. return static::find($arrOptions);
  822. }
  823. /**
  824. * Find all records
  825. *
  826. * @param array $arrOptions An optional options array
  827. *
  828. * @return Collection|null The model collection or null if the result is empty
  829. */
  830. public static function findAll(array $arrOptions=array())
  831. {
  832. $arrOptions = array_merge
  833. (
  834. array
  835. (
  836. 'return' => 'Collection'
  837. ),
  838. $arrOptions
  839. );
  840. return static::find($arrOptions);
  841. }
  842. /**
  843. * Magic method to map Model::findByName() to Model::findBy('name')
  844. *
  845. * @param string $name The method name
  846. * @param array $args The passed arguments
  847. *
  848. * @return static|Collection|integer|null A model or model collection
  849. *
  850. * @throws \Exception If the method name is invalid
  851. */
  852. public static function __callStatic($name, $args)
  853. {
  854. if (strncmp($name, 'findBy', 6) === 0)
  855. {
  856. array_unshift($args, lcfirst(substr($name, 6)));
  857. return static::findBy(...$args);
  858. }
  859. if (strncmp($name, 'findOneBy', 9) === 0)
  860. {
  861. array_unshift($args, lcfirst(substr($name, 9)));
  862. return static::findOneBy(...$args);
  863. }
  864. if (strncmp($name, 'countBy', 7) === 0)
  865. {
  866. array_unshift($args, lcfirst(substr($name, 7)));
  867. return static::countBy(...$args);
  868. }
  869. throw new \Exception("Unknown method $name");
  870. }
  871. /**
  872. * Find records and return the model or model collection
  873. *
  874. * Supported options:
  875. *
  876. * * column: the field name
  877. * * value: the field value
  878. * * limit: the maximum number of rows
  879. * * offset: the number of rows to skip
  880. * * order: the sorting order
  881. * * eager: load all related records eagerly
  882. *
  883. * @param array $arrOptions The options array
  884. *
  885. * @return Model|Model[]|Collection|null A model, model collection or null if the result is empty
  886. */
  887. protected static function find(array $arrOptions)
  888. {
  889. if (!static::$strTable)
  890. {
  891. return null;
  892. }
  893. // Try to load from the registry
  894. if (($arrOptions['return'] ?? null) == 'Model')
  895. {
  896. $arrColumn = (array) $arrOptions['column'];
  897. if (\count($arrColumn) == 1)
  898. {
  899. // Support table prefixes
  900. $arrColumn[0] = preg_replace('/^' . preg_quote(static::getTable(), '/') . '\./', '', $arrColumn[0]);
  901. if ($arrColumn[0] == static::$strPk || \in_array($arrColumn[0], static::getUniqueFields()))
  902. {
  903. $varKey = \is_array($arrOptions['value'] ?? null) ? $arrOptions['value'][0] : ($arrOptions['value'] ?? null);
  904. $objModel = Registry::getInstance()->fetch(static::$strTable, $varKey, $arrColumn[0]);
  905. if ($objModel !== null)
  906. {
  907. return $objModel;
  908. }
  909. }
  910. }
  911. }
  912. $arrOptions['table'] = static::$strTable;
  913. $strQuery = static::buildFindQuery($arrOptions);
  914. $objStatement = Database::getInstance()->prepare($strQuery);
  915. // Defaults for limit and offset
  916. if (!isset($arrOptions['limit']))
  917. {
  918. $arrOptions['limit'] = 0;
  919. }
  920. if (!isset($arrOptions['offset']))
  921. {
  922. $arrOptions['offset'] = 0;
  923. }
  924. // Limit
  925. if ($arrOptions['limit'] > 0 || $arrOptions['offset'] > 0)
  926. {
  927. $objStatement->limit($arrOptions['limit'], $arrOptions['offset']);
  928. }
  929. if (!\array_key_exists('value', $arrOptions))
  930. {
  931. $arrOptions['value'] = array();
  932. }
  933. $objStatement = static::preFind($objStatement);
  934. $objResult = $objStatement->execute(...array_values(\is_array($arrOptions['value']) ? $arrOptions['value'] : array($arrOptions['value'])));
  935. if ($objResult->numRows < 1)
  936. {
  937. return ($arrOptions['return'] ?? null) == 'Array' ? array() : null;
  938. }
  939. $objResult = static::postFind($objResult);
  940. // Try to load from the registry
  941. if (($arrOptions['return'] ?? null) == 'Model')
  942. {
  943. $objModel = Registry::getInstance()->fetch(static::$strTable, $objResult->{static::$strPk});
  944. if ($objModel !== null)
  945. {
  946. return $objModel->mergeRow($objResult->row());
  947. }
  948. return static::createModelFromDbResult($objResult);
  949. }
  950. if (($arrOptions['return'] ?? null) == 'Array')
  951. {
  952. return static::createCollectionFromDbResult($objResult, static::$strTable)->getModels();
  953. }
  954. return static::createCollectionFromDbResult($objResult, static::$strTable);
  955. }
  956. /**
  957. * Modify the database statement before it is executed
  958. *
  959. * @param Statement $objStatement The database statement object
  960. *
  961. * @return Statement The database statement object
  962. */
  963. protected static function preFind(Statement $objStatement)
  964. {
  965. return $objStatement;
  966. }
  967. /**
  968. * Modify the database result before the model is created
  969. *
  970. * @param Result $objResult The database result object
  971. *
  972. * @return Result The database result object
  973. */
  974. protected static function postFind(Result $objResult)
  975. {
  976. return $objResult;
  977. }
  978. /**
  979. * Return the number of records matching certain criteria
  980. *
  981. * @param mixed $strColumn An optional property name
  982. * @param mixed $varValue An optional property value
  983. * @param array $arrOptions An optional options array
  984. *
  985. * @return integer The number of matching rows
  986. */
  987. public static function countBy($strColumn=null, $varValue=null, array $arrOptions=array())
  988. {
  989. if (!static::$strTable)
  990. {
  991. return 0;
  992. }
  993. $arrOptions = array_merge
  994. (
  995. array
  996. (
  997. 'table' => static::$strTable,
  998. 'column' => $strColumn,
  999. 'value' => $varValue
  1000. ),
  1001. $arrOptions
  1002. );
  1003. $strQuery = static::buildCountQuery($arrOptions);
  1004. return (int) Database::getInstance()->prepare($strQuery)->execute(...(array) ($arrOptions['value'] ?? array()))->count;
  1005. }
  1006. /**
  1007. * Return the total number of rows
  1008. *
  1009. * @return integer The total number of rows
  1010. */
  1011. public static function countAll()
  1012. {
  1013. return static::countBy();
  1014. }
  1015. /**
  1016. * Compile a Model class name from a table name (e.g. tl_form_field becomes FormFieldModel)
  1017. *
  1018. * @param string $strTable The table name
  1019. *
  1020. * @return class-string<Model> The model class name
  1021. */
  1022. public static function getClassFromTable($strTable)
  1023. {
  1024. if (isset(static::$arrClassNames[$strTable]))
  1025. {
  1026. return static::$arrClassNames[$strTable];
  1027. }
  1028. if (isset($GLOBALS['TL_MODELS'][$strTable]))
  1029. {
  1030. static::$arrClassNames[$strTable] = $GLOBALS['TL_MODELS'][$strTable]; // see 4796
  1031. return static::$arrClassNames[$strTable];
  1032. }
  1033. trigger_deprecation('contao/core-bundle', '4.10', sprintf('Not registering table "%s" in $GLOBALS[\'TL_MODELS\'] has been deprecated and will no longer work in Contao 5.0.', $strTable));
  1034. $arrChunks = explode('_', $strTable);
  1035. if ($arrChunks[0] == 'tl')
  1036. {
  1037. array_shift($arrChunks);
  1038. }
  1039. static::$arrClassNames[$strTable] = implode('', array_map('ucfirst', $arrChunks)) . 'Model';
  1040. return static::$arrClassNames[$strTable];
  1041. }
  1042. /**
  1043. * Build a query based on the given options
  1044. *
  1045. * @param array $arrOptions The options array
  1046. *
  1047. * @return string The query string
  1048. */
  1049. protected static function buildFindQuery(array $arrOptions)
  1050. {
  1051. return QueryBuilder::find($arrOptions);
  1052. }
  1053. /**
  1054. * Build a query based on the given options to count the number of records
  1055. *
  1056. * @param array $arrOptions The options array
  1057. *
  1058. * @return string The query string
  1059. */
  1060. protected static function buildCountQuery(array $arrOptions)
  1061. {
  1062. return QueryBuilder::count($arrOptions);
  1063. }
  1064. /**
  1065. * Create a model from a database result
  1066. *
  1067. * @param Result $objResult The database result object
  1068. *
  1069. * @return static The model
  1070. */
  1071. protected static function createModelFromDbResult(Result $objResult)
  1072. {
  1073. /**
  1074. * @var static $strClass
  1075. * @var class-string<static> $strClass
  1076. */
  1077. $strClass = static::getClassFromTable(static::$strTable);
  1078. return new $strClass($objResult);
  1079. }
  1080. /**
  1081. * Create a Collection object
  1082. *
  1083. * @param array $arrModels An array of models
  1084. * @param string $strTable The table name
  1085. *
  1086. * @return Collection The Collection object
  1087. */
  1088. protected static function createCollection(array $arrModels, $strTable)
  1089. {
  1090. return new Collection($arrModels, $strTable);
  1091. }
  1092. /**
  1093. * Create a new collection from a database result
  1094. *
  1095. * @param Result $objResult The database result object
  1096. * @param string $strTable The table name
  1097. *
  1098. * @return Collection The model collection
  1099. */
  1100. protected static function createCollectionFromDbResult(Result $objResult, $strTable)
  1101. {
  1102. return Collection::createFromDbResult($objResult, $strTable);
  1103. }
  1104. /**
  1105. * Check if the preview mode is enabled
  1106. *
  1107. * @param array $arrOptions The options array
  1108. *
  1109. * @return boolean
  1110. */
  1111. protected static function isPreviewMode(array $arrOptions)
  1112. {
  1113. if (isset($arrOptions['ignoreFePreview']))
  1114. {
  1115. return false;
  1116. }
  1117. return System::getContainer()->get('contao.security.token_checker')->isPreviewMode();
  1118. }
  1119. /**
  1120. * This method is a hot path so caching the keys gets rid of thousands of str_contains() calls.
  1121. */
  1122. protected static function isJoinedField(string $key): bool
  1123. {
  1124. static $cache = array();
  1125. return $cache[$key] ??= str_contains($key, '__');
  1126. }
  1127. }
  1128. class_alias(Model::class, 'Model');