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

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 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.  * @author Leo Feyer <https://github.com/leofeyer>
  42.  */
  43. abstract class Model
  44. {
  45.     /**
  46.      * Insert flag
  47.      * @var integer
  48.      */
  49.     const INSERT 1;
  50.     /**
  51.      * Update flag
  52.      * @var integer
  53.      */
  54.     const UPDATE 2;
  55.     /**
  56.      * Table name
  57.      * @var string
  58.      */
  59.     protected static $strTable;
  60.     /**
  61.      * Primary key
  62.      * @var string
  63.      */
  64.     protected static $strPk 'id';
  65.     /**
  66.      * Class name cache
  67.      * @var array
  68.      */
  69.     protected static $arrClassNames = array();
  70.     /**
  71.      * Data
  72.      * @var array
  73.      */
  74.     protected $arrData = array();
  75.     /**
  76.      * Modified keys
  77.      * @var array
  78.      */
  79.     protected $arrModified = array();
  80.     /**
  81.      * Relations
  82.      * @var array
  83.      */
  84.     protected $arrRelations = array();
  85.     /**
  86.      * Related
  87.      * @var array
  88.      */
  89.     protected $arrRelated = array();
  90.     /**
  91.      * Prevent saving
  92.      * @var boolean
  93.      */
  94.     protected $blnPreventSaving false;
  95.     /**
  96.      * Load the relations and optionally process a result set
  97.      *
  98.      * @param Result|array $objResult An optional database result or array
  99.      */
  100.     public function __construct($objResult=null)
  101.     {
  102.         $this->arrModified = array();
  103.         $objDca DcaExtractor::getInstance(static::$strTable);
  104.         $this->arrRelations $objDca->getRelations();
  105.         if ($objResult !== null)
  106.         {
  107.             $arrRelated = array();
  108.             if ($objResult instanceof Result)
  109.             {
  110.                 $arrData $objResult->row();
  111.             }
  112.             else
  113.             {
  114.                 $arrData = (array) $objResult;
  115.             }
  116.             // Look for joined fields
  117.             foreach ($arrData as $k=>$v)
  118.             {
  119.                 if (strpos($k'__') !== false)
  120.                 {
  121.                     list($key$field) = explode('__'$k2);
  122.                     if (!isset($arrRelated[$key]))
  123.                     {
  124.                         $arrRelated[$key] = array();
  125.                     }
  126.                     $arrRelated[$key][$field] = $v;
  127.                     unset($arrData[$k]);
  128.                 }
  129.             }
  130.             $objRegistry Registry::getInstance();
  131.             $this->setRow($arrData); // see #5439
  132.             $objRegistry->register($this);
  133.             // Create the related models
  134.             foreach ($arrRelated as $key=>$row)
  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 ($this->$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 (strpos($k'__') !== false)
  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 (strpos($k'__') !== false)
  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 fiels
  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 fiels
  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->$strKeytrue);
  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.         // Try to load from the registry
  646.         if (empty($arrOptions))
  647.         {
  648.             $objModel Registry::getInstance()->fetch(static::$strTable$varValue);
  649.             if ($objModel !== null)
  650.             {
  651.                 return $objModel;
  652.             }
  653.         }
  654.         $arrOptions array_merge
  655.         (
  656.             array
  657.             (
  658.                 'limit'  => 1,
  659.                 'column' => static::$strPk,
  660.                 'value'  => $varValue,
  661.                 'return' => 'Model'
  662.             ),
  663.             $arrOptions
  664.         );
  665.         return static::find($arrOptions);
  666.     }
  667.     /**
  668.      * Find a single record by its ID or alias
  669.      *
  670.      * @param mixed $varId      The ID or alias
  671.      * @param array $arrOptions An optional options array
  672.      *
  673.      * @return static The model or null if the result is empty
  674.      */
  675.     public static function findByIdOrAlias($varId, array $arrOptions=array())
  676.     {
  677.         $isAlias = !preg_match('/^[1-9]\d*$/'$varId);
  678.         // Try to load from the registry
  679.         if (!$isAlias && empty($arrOptions))
  680.         {
  681.             $objModel Registry::getInstance()->fetch(static::$strTable$varId);
  682.             if ($objModel !== null)
  683.             {
  684.                 return $objModel;
  685.             }
  686.         }
  687.         $t = static::$strTable;
  688.         $arrOptions array_merge
  689.         (
  690.             array
  691.             (
  692.                 'limit'  => 1,
  693.                 'column' => $isAlias ? array("BINARY $t.alias=?") : array("$t.id=?"),
  694.                 'value'  => $varId,
  695.                 'return' => 'Model'
  696.             ),
  697.             $arrOptions
  698.         );
  699.         return static::find($arrOptions);
  700.     }
  701.     /**
  702.      * Find multiple records by their IDs
  703.      *
  704.      * @param array $arrIds     An array of IDs
  705.      * @param array $arrOptions An optional options array
  706.      *
  707.      * @return Collection|null The model collection or null if there are no records
  708.      */
  709.     public static function findMultipleByIds($arrIds, array $arrOptions=array())
  710.     {
  711.         if (empty($arrIds) || !\is_array($arrIds))
  712.         {
  713.             return null;
  714.         }
  715.         $arrRegistered = array();
  716.         $arrUnregistered = array();
  717.         // Search for registered models
  718.         foreach ($arrIds as $intId)
  719.         {
  720.             if (empty($arrOptions))
  721.             {
  722.                 $arrRegistered[$intId] = Registry::getInstance()->fetch(static::$strTable$intId);
  723.             }
  724.             if (!isset($arrRegistered[$intId]))
  725.             {
  726.                 $arrUnregistered[] = $intId;
  727.             }
  728.         }
  729.         // Fetch only the missing models from the database
  730.         if (!empty($arrUnregistered))
  731.         {
  732.             $t = static::$strTable;
  733.             $arrOptions array_merge
  734.             (
  735.                 array
  736.                 (
  737.                     'column' => array("$t.id IN(" implode(','array_map('\intval'$arrUnregistered)) . ")"),
  738.                     'value'  => null,
  739.                     'order'  => Database::getInstance()->findInSet("$t.id"$arrIds),
  740.                     'return' => 'Collection'
  741.                 ),
  742.                 $arrOptions
  743.             );
  744.             $objMissing = static::find($arrOptions);
  745.             if ($objMissing !== null)
  746.             {
  747.                 foreach ($objMissing as $objCurrent)
  748.                 {
  749.                     $intId $objCurrent->{static::$strPk};
  750.                     $arrRegistered[$intId] = $objCurrent;
  751.                 }
  752.             }
  753.         }
  754.         $arrRegistered array_filter(array_values($arrRegistered));
  755.         if (empty($arrRegistered))
  756.         {
  757.             return null;
  758.         }
  759.         return static::createCollection($arrRegistered, static::$strTable);
  760.     }
  761.     /**
  762.      * Find a single record by various criteria
  763.      *
  764.      * @param mixed $strColumn  The property name
  765.      * @param mixed $varValue   The property value
  766.      * @param array $arrOptions An optional options array
  767.      *
  768.      * @return static The model or null if the result is empty
  769.      */
  770.     public static function findOneBy($strColumn$varValue, array $arrOptions=array())
  771.     {
  772.         $arrOptions array_merge
  773.         (
  774.             array
  775.             (
  776.                 'limit'  => 1,
  777.                 'column' => $strColumn,
  778.                 'value'  => $varValue,
  779.                 'return' => 'Model'
  780.             ),
  781.             $arrOptions
  782.         );
  783.         return static::find($arrOptions);
  784.     }
  785.     /**
  786.      * Find records by various criteria
  787.      *
  788.      * @param mixed $strColumn  The property name
  789.      * @param mixed $varValue   The property value
  790.      * @param array $arrOptions An optional options array
  791.      *
  792.      * @return static|Collection|null A model, model collection or null if the result is empty
  793.      */
  794.     public static function findBy($strColumn$varValue, array $arrOptions=array())
  795.     {
  796.         $blnModel false;
  797.         $arrColumn = (array) $strColumn;
  798.         if (\count($arrColumn) == && ($arrColumn[0] === static::getPk() || \in_array($arrColumn[0], static::getUniqueFields())))
  799.         {
  800.             $blnModel true;
  801.         }
  802.         $arrOptions array_merge
  803.         (
  804.             array
  805.             (
  806.                 'column' => $strColumn,
  807.                 'value'  => $varValue,
  808.                 'return' => $blnModel 'Model' 'Collection'
  809.             ),
  810.             $arrOptions
  811.         );
  812.         return static::find($arrOptions);
  813.     }
  814.     /**
  815.      * Find all records
  816.      *
  817.      * @param array $arrOptions An optional options array
  818.      *
  819.      * @return Collection|null The model collection or null if the result is empty
  820.      */
  821.     public static function findAll(array $arrOptions=array())
  822.     {
  823.         $arrOptions array_merge
  824.         (
  825.             array
  826.             (
  827.                 'return' => 'Collection'
  828.             ),
  829.             $arrOptions
  830.         );
  831.         return static::find($arrOptions);
  832.     }
  833.     /**
  834.      * Magic method to map Model::findByName() to Model::findBy('name')
  835.      *
  836.      * @param string $name The method name
  837.      * @param array  $args The passed arguments
  838.      *
  839.      * @return static|Collection|integer|null A model or model collection
  840.      *
  841.      * @throws \Exception If the method name is invalid
  842.      */
  843.     public static function __callStatic($name$args)
  844.     {
  845.         if (strncmp($name'findBy'6) === 0)
  846.         {
  847.             array_unshift($argslcfirst(substr($name6)));
  848.             return static::findBy(...$args);
  849.         }
  850.         if (strncmp($name'findOneBy'9) === 0)
  851.         {
  852.             array_unshift($argslcfirst(substr($name9)));
  853.             return static::findOneBy(...$args);
  854.         }
  855.         if (strncmp($name'countBy'7) === 0)
  856.         {
  857.             array_unshift($argslcfirst(substr($name7)));
  858.             return static::countBy(...$args);
  859.         }
  860.         throw new \Exception("Unknown method $name");
  861.     }
  862.     /**
  863.      * Find records and return the model or model collection
  864.      *
  865.      * Supported options:
  866.      *
  867.      * * column: the field name
  868.      * * value:  the field value
  869.      * * limit:  the maximum number of rows
  870.      * * offset: the number of rows to skip
  871.      * * order:  the sorting order
  872.      * * eager:  load all related records eagerly
  873.      *
  874.      * @param array $arrOptions The options array
  875.      *
  876.      * @return Model|Model[]|Collection|null A model, model collection or null if the result is empty
  877.      */
  878.     protected static function find(array $arrOptions)
  879.     {
  880.         if (!static::$strTable)
  881.         {
  882.             return null;
  883.         }
  884.         // Try to load from the registry
  885.         if ($arrOptions['return'] == 'Model')
  886.         {
  887.             $arrColumn = (array) $arrOptions['column'];
  888.             if (\count($arrColumn) == 1)
  889.             {
  890.                 // Support table prefixes
  891.                 $arrColumn[0] = preg_replace('/^' preg_quote(static::getTable(), '/') . '\./'''$arrColumn[0]);
  892.                 if ($arrColumn[0] == static::$strPk || \in_array($arrColumn[0], static::getUniqueFields()))
  893.                 {
  894.                     $varKey = \is_array($arrOptions['value']) ? $arrOptions['value'][0] : $arrOptions['value'];
  895.                     $objModel Registry::getInstance()->fetch(static::$strTable$varKey$arrColumn[0]);
  896.                     if ($objModel !== null)
  897.                     {
  898.                         return $objModel;
  899.                     }
  900.                 }
  901.             }
  902.         }
  903.         $arrOptions['table'] = static::$strTable;
  904.         $strQuery = static::buildFindQuery($arrOptions);
  905.         $objStatement Database::getInstance()->prepare($strQuery);
  906.         // Defaults for limit and offset
  907.         if (!isset($arrOptions['limit']))
  908.         {
  909.             $arrOptions['limit'] = 0;
  910.         }
  911.         if (!isset($arrOptions['offset']))
  912.         {
  913.             $arrOptions['offset'] = 0;
  914.         }
  915.         // Limit
  916.         if ($arrOptions['limit'] > || $arrOptions['offset'] > 0)
  917.         {
  918.             $objStatement->limit($arrOptions['limit'], $arrOptions['offset']);
  919.         }
  920.         $objStatement = static::preFind($objStatement);
  921.         $objResult $objStatement->execute($arrOptions['value']);
  922.         if ($objResult->numRows 1)
  923.         {
  924.             return $arrOptions['return'] == 'Array' ? array() : null;
  925.         }
  926.         $objResult = static::postFind($objResult);
  927.         // Try to load from the registry
  928.         if ($arrOptions['return'] == 'Model')
  929.         {
  930.             $objModel Registry::getInstance()->fetch(static::$strTable$objResult->{static::$strPk});
  931.             if ($objModel !== null)
  932.             {
  933.                 return $objModel->mergeRow($objResult->row());
  934.             }
  935.             return static::createModelFromDbResult($objResult);
  936.         }
  937.         if ($arrOptions['return'] == 'Array')
  938.         {
  939.             return static::createCollectionFromDbResult($objResult, static::$strTable)->getModels();
  940.         }
  941.         return static::createCollectionFromDbResult($objResult, static::$strTable);
  942.     }
  943.     /**
  944.      * Modify the database statement before it is executed
  945.      *
  946.      * @param Statement $objStatement The database statement object
  947.      *
  948.      * @return Statement The database statement object
  949.      */
  950.     protected static function preFind(Statement $objStatement)
  951.     {
  952.         return $objStatement;
  953.     }
  954.     /**
  955.      * Modify the database result before the model is created
  956.      *
  957.      * @param Result $objResult The database result object
  958.      *
  959.      * @return Result The database result object
  960.      */
  961.     protected static function postFind(Result $objResult)
  962.     {
  963.         return $objResult;
  964.     }
  965.     /**
  966.      * Return the number of records matching certain criteria
  967.      *
  968.      * @param mixed $strColumn  An optional property name
  969.      * @param mixed $varValue   An optional property value
  970.      * @param array $arrOptions An optional options array
  971.      *
  972.      * @return integer The number of matching rows
  973.      */
  974.     public static function countBy($strColumn=null$varValue=null, array $arrOptions=array())
  975.     {
  976.         if (!static::$strTable)
  977.         {
  978.             return 0;
  979.         }
  980.         $arrOptions array_merge
  981.         (
  982.             array
  983.             (
  984.                 'table'  => static::$strTable,
  985.                 'column' => $strColumn,
  986.                 'value'  => $varValue
  987.             ),
  988.             $arrOptions
  989.         );
  990.         $strQuery = static::buildCountQuery($arrOptions);
  991.         return (int) Database::getInstance()->prepare($strQuery)->execute($arrOptions['value'])->count;
  992.     }
  993.     /**
  994.      * Return the total number of rows
  995.      *
  996.      * @return integer The total number of rows
  997.      */
  998.     public static function countAll()
  999.     {
  1000.         return static::countBy();
  1001.     }
  1002.     /**
  1003.      * Compile a Model class name from a table name (e.g. tl_form_field becomes FormFieldModel)
  1004.      *
  1005.      * @param string $strTable The table name
  1006.      *
  1007.      * @return string The model class name
  1008.      */
  1009.     public static function getClassFromTable($strTable)
  1010.     {
  1011.         if (isset(static::$arrClassNames[$strTable]))
  1012.         {
  1013.             return static::$arrClassNames[$strTable];
  1014.         }
  1015.         if (isset($GLOBALS['TL_MODELS'][$strTable]))
  1016.         {
  1017.             static::$arrClassNames[$strTable] = $GLOBALS['TL_MODELS'][$strTable]; // see 4796
  1018.             return static::$arrClassNames[$strTable];
  1019.         }
  1020.         $arrChunks explode('_'$strTable);
  1021.         if ($arrChunks[0] == 'tl')
  1022.         {
  1023.             array_shift($arrChunks);
  1024.         }
  1025.         static::$arrClassNames[$strTable] = implode(''array_map('ucfirst'$arrChunks)) . 'Model';
  1026.         return static::$arrClassNames[$strTable];
  1027.     }
  1028.     /**
  1029.      * Build a query based on the given options
  1030.      *
  1031.      * @param array $arrOptions The options array
  1032.      *
  1033.      * @return string The query string
  1034.      */
  1035.     protected static function buildFindQuery(array $arrOptions)
  1036.     {
  1037.         return QueryBuilder::find($arrOptions);
  1038.     }
  1039.     /**
  1040.      * Build a query based on the given options to count the number of records
  1041.      *
  1042.      * @param array $arrOptions The options array
  1043.      *
  1044.      * @return string The query string
  1045.      */
  1046.     protected static function buildCountQuery(array $arrOptions)
  1047.     {
  1048.         return QueryBuilder::count($arrOptions);
  1049.     }
  1050.     /**
  1051.      * Create a model from a database result
  1052.      *
  1053.      * @param Result $objResult The database result object
  1054.      *
  1055.      * @return static The model
  1056.      */
  1057.     protected static function createModelFromDbResult(Result $objResult)
  1058.     {
  1059.         /** @var static $strClass */
  1060.         $strClass = static::getClassFromTable(static::$strTable);
  1061.         return new $strClass($objResult);
  1062.     }
  1063.     /**
  1064.      * Create a Collection object
  1065.      *
  1066.      * @param array  $arrModels An array of models
  1067.      * @param string $strTable  The table name
  1068.      *
  1069.      * @return Collection The Collection object
  1070.      */
  1071.     protected static function createCollection(array $arrModels$strTable)
  1072.     {
  1073.         return new Collection($arrModels$strTable);
  1074.     }
  1075.     /**
  1076.      * Create a new collection from a database result
  1077.      *
  1078.      * @param Result $objResult The database result object
  1079.      * @param string $strTable  The table name
  1080.      *
  1081.      * @return Collection The model collection
  1082.      */
  1083.     protected static function createCollectionFromDbResult(Result $objResult$strTable)
  1084.     {
  1085.         return Collection::createFromDbResult($objResult$strTable);
  1086.     }
  1087.     /**
  1088.      * Check if the preview mode is enabled
  1089.      *
  1090.      * @param array $arrOptions The options array
  1091.      *
  1092.      * @return boolean
  1093.      */
  1094.     protected static function isPreviewMode(array $arrOptions)
  1095.     {
  1096.         if (isset($arrOptions['ignoreFePreview']))
  1097.         {
  1098.             return false;
  1099.         }
  1100.         return \defined('BE_USER_LOGGED_IN') && BE_USER_LOGGED_IN === true;
  1101.     }
  1102. }
  1103. class_alias(Model::class, 'Model');