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

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.             throw new \Exception("Field $strKey does not seem to be related");
  502.         }
  503.         // The relation exists but there is no reference yet (see #6161 and #458)
  504.         if (empty($this->$strKey))
  505.         {
  506.             return null;
  507.         }
  508.         $arrRelation $this->arrRelations[$strKey];
  509.         /** @var static $strClass */
  510.         $strClass = static::getClassFromTable($arrRelation['table']);
  511.         // Load the related record(s)
  512.         if ($arrRelation['type'] == 'hasOne' || $arrRelation['type'] == 'belongsTo')
  513.         {
  514.             $this->arrRelated[$strKey] = $strClass::findOneBy($arrRelation['field'], $this->$strKey$arrOptions);
  515.         }
  516.         elseif ($arrRelation['type'] == 'hasMany' || $arrRelation['type'] == 'belongsToMany')
  517.         {
  518.             if (isset($arrRelation['delimiter']))
  519.             {
  520.                 $arrValues StringUtil::trimsplit($arrRelation['delimiter'], $this->$strKey);
  521.             }
  522.             else
  523.             {
  524.                 $arrValues StringUtil::deserialize($this->$strKeytrue);
  525.             }
  526.             $objModel null;
  527.             if (\is_array($arrValues))
  528.             {
  529.                 // Handle UUIDs (see #6525 and #8850)
  530.                 if ($arrRelation['table'] == 'tl_files' && $arrRelation['field'] == 'uuid')
  531.                 {
  532.                     /** @var FilesModel $strClass */
  533.                     $objModel $strClass::findMultipleByUuids($arrValues$arrOptions);
  534.                 }
  535.                 else
  536.                 {
  537.                     $strField $arrRelation['table'] . '.' Database::quoteIdentifier($arrRelation['field']);
  538.                     $arrOptions array_merge
  539.                     (
  540.                         array
  541.                         (
  542.                             'order' => Database::getInstance()->findInSet($strField$arrValues)
  543.                         ),
  544.                         $arrOptions
  545.                     );
  546.                     $objModel $strClass::findBy(array($strField " IN('" implode("','"$arrValues) . "')"), null$arrOptions);
  547.                 }
  548.             }
  549.             $this->arrRelated[$strKey] = $objModel;
  550.         }
  551.         return $this->arrRelated[$strKey];
  552.     }
  553.     /**
  554.      * Reload the data from the database discarding all modifications
  555.      */
  556.     public function refresh()
  557.     {
  558.         // Track primary key changes
  559.         $intPk $this->arrModified[static::$strPk] ?? $this->{static::$strPk};
  560.         // Reload the database record
  561.         $res Database::getInstance()->prepare("SELECT * FROM " . static::$strTable " WHERE " Database::quoteIdentifier(static::$strPk) . "=?")
  562.                                        ->execute($intPk);
  563.         $this->setRow($res->row());
  564.     }
  565.     /**
  566.      * Detach the model from the registry
  567.      *
  568.      * @param boolean $blnKeepClone Keeps a clone of the model in the registry
  569.      */
  570.     public function detach($blnKeepClone=true)
  571.     {
  572.         $registry Registry::getInstance();
  573.         if (!$registry->isRegistered($this))
  574.         {
  575.             return;
  576.         }
  577.         $registry->unregister($this);
  578.         if ($blnKeepClone)
  579.         {
  580.             $this->cloneOriginal()->attach();
  581.         }
  582.     }
  583.     /**
  584.      * Attach the model to the registry
  585.      */
  586.     public function attach()
  587.     {
  588.         Registry::getInstance()->register($this);
  589.     }
  590.     /**
  591.      * Called when the model is attached to the model registry
  592.      *
  593.      * @param Registry $registry The model registry
  594.      */
  595.     public function onRegister(Registry $registry)
  596.     {
  597.         // Register aliases to unique fields
  598.         foreach (static::getUniqueFields() as $strColumn)
  599.         {
  600.             $varAliasValue $this->{$strColumn};
  601.             if (!$registry->isRegisteredAlias($this$strColumn$varAliasValue))
  602.             {
  603.                 $registry->registerAlias($this$strColumn$varAliasValue);
  604.             }
  605.         }
  606.     }
  607.     /**
  608.      * Called when the model is detached from the model registry
  609.      *
  610.      * @param Registry $registry The model registry
  611.      */
  612.     public function onUnregister(Registry $registry)
  613.     {
  614.         // Unregister aliases to unique fields
  615.         foreach (static::getUniqueFields() as $strColumn)
  616.         {
  617.             $varAliasValue $this->{$strColumn};
  618.             if ($registry->isRegisteredAlias($this$strColumn$varAliasValue))
  619.             {
  620.                 $registry->unregisterAlias($this$strColumn$varAliasValue);
  621.             }
  622.         }
  623.     }
  624.     /**
  625.      * Prevent saving the model
  626.      *
  627.      * @param boolean $blnKeepClone Keeps a clone of the model in the registry
  628.      */
  629.     public function preventSaving($blnKeepClone=true)
  630.     {
  631.         $this->detach($blnKeepClone);
  632.         $this->blnPreventSaving true;
  633.     }
  634.     /**
  635.      * Find a single record by its primary key
  636.      *
  637.      * @param mixed $varValue   The property value
  638.      * @param array $arrOptions An optional options array
  639.      *
  640.      * @return static The model or null if the result is empty
  641.      */
  642.     public static function findByPk($varValue, array $arrOptions=array())
  643.     {
  644.         // Try to load from the registry
  645.         if (empty($arrOptions))
  646.         {
  647.             $objModel Registry::getInstance()->fetch(static::$strTable$varValue);
  648.             if ($objModel !== null)
  649.             {
  650.                 return $objModel;
  651.             }
  652.         }
  653.         $arrOptions array_merge
  654.         (
  655.             array
  656.             (
  657.                 'limit'  => 1,
  658.                 'column' => static::$strPk,
  659.                 'value'  => $varValue,
  660.                 'return' => 'Model'
  661.             ),
  662.             $arrOptions
  663.         );
  664.         return static::find($arrOptions);
  665.     }
  666.     /**
  667.      * Find a single record by its ID or alias
  668.      *
  669.      * @param mixed $varId      The ID or alias
  670.      * @param array $arrOptions An optional options array
  671.      *
  672.      * @return static The model or null if the result is empty
  673.      */
  674.     public static function findByIdOrAlias($varId, array $arrOptions=array())
  675.     {
  676.         $isAlias = !preg_match('/^[1-9]\d*$/'$varId);
  677.         // Try to load from the registry
  678.         if (!$isAlias && empty($arrOptions))
  679.         {
  680.             $objModel Registry::getInstance()->fetch(static::$strTable$varId);
  681.             if ($objModel !== null)
  682.             {
  683.                 return $objModel;
  684.             }
  685.         }
  686.         $t = static::$strTable;
  687.         $arrOptions array_merge
  688.         (
  689.             array
  690.             (
  691.                 'limit'  => 1,
  692.                 'column' => $isAlias ? array("BINARY $t.alias=?") : array("$t.id=?"),
  693.                 'value'  => $varId,
  694.                 'return' => 'Model'
  695.             ),
  696.             $arrOptions
  697.         );
  698.         return static::find($arrOptions);
  699.     }
  700.     /**
  701.      * Find multiple records by their IDs
  702.      *
  703.      * @param array $arrIds     An array of IDs
  704.      * @param array $arrOptions An optional options array
  705.      *
  706.      * @return Collection|null The model collection or null if there are no records
  707.      */
  708.     public static function findMultipleByIds($arrIds, array $arrOptions=array())
  709.     {
  710.         if (empty($arrIds) || !\is_array($arrIds))
  711.         {
  712.             return null;
  713.         }
  714.         $arrRegistered = array();
  715.         $arrUnregistered = array();
  716.         // Search for registered models
  717.         foreach ($arrIds as $intId)
  718.         {
  719.             if (empty($arrOptions))
  720.             {
  721.                 $arrRegistered[$intId] = Registry::getInstance()->fetch(static::$strTable$intId);
  722.             }
  723.             if (!isset($arrRegistered[$intId]))
  724.             {
  725.                 $arrUnregistered[] = $intId;
  726.             }
  727.         }
  728.         // Fetch only the missing models from the database
  729.         if (!empty($arrUnregistered))
  730.         {
  731.             $t = static::$strTable;
  732.             $arrOptions array_merge
  733.             (
  734.                 array
  735.                 (
  736.                     'column' => array("$t.id IN(" implode(','array_map('\intval'$arrUnregistered)) . ")"),
  737.                     'value'  => null,
  738.                     'order'  => Database::getInstance()->findInSet("$t.id"$arrIds),
  739.                     'return' => 'Collection'
  740.                 ),
  741.                 $arrOptions
  742.             );
  743.             $objMissing = static::find($arrOptions);
  744.             if ($objMissing !== null)
  745.             {
  746.                 while ($objMissing->next())
  747.                 {
  748.                     $intId $objMissing->{static::$strPk};
  749.                     $arrRegistered[$intId] = $objMissing->current();
  750.                 }
  751.             }
  752.         }
  753.         $arrRegistered array_filter(array_values($arrRegistered));
  754.         if (empty($arrRegistered))
  755.         {
  756.             return null;
  757.         }
  758.         return static::createCollection($arrRegistered, static::$strTable);
  759.     }
  760.     /**
  761.      * Find a single record by various criteria
  762.      *
  763.      * @param mixed $strColumn  The property name
  764.      * @param mixed $varValue   The property value
  765.      * @param array $arrOptions An optional options array
  766.      *
  767.      * @return static The model or null if the result is empty
  768.      */
  769.     public static function findOneBy($strColumn$varValue, array $arrOptions=array())
  770.     {
  771.         $arrOptions array_merge
  772.         (
  773.             array
  774.             (
  775.                 'limit'  => 1,
  776.                 'column' => $strColumn,
  777.                 'value'  => $varValue,
  778.                 'return' => 'Model'
  779.             ),
  780.             $arrOptions
  781.         );
  782.         return static::find($arrOptions);
  783.     }
  784.     /**
  785.      * Find records by various criteria
  786.      *
  787.      * @param mixed $strColumn  The property name
  788.      * @param mixed $varValue   The property value
  789.      * @param array $arrOptions An optional options array
  790.      *
  791.      * @return static|Collection|null A model, model collection or null if the result is empty
  792.      */
  793.     public static function findBy($strColumn$varValue, array $arrOptions=array())
  794.     {
  795.         $blnModel false;
  796.         $arrColumn = (array) $strColumn;
  797.         if (\count($arrColumn) == && ($arrColumn[0] === static::getPk() || \in_array($arrColumn[0], static::getUniqueFields())))
  798.         {
  799.             $blnModel true;
  800.         }
  801.         $arrOptions array_merge
  802.         (
  803.             array
  804.             (
  805.                 'column' => $strColumn,
  806.                 'value'  => $varValue,
  807.                 'return' => $blnModel 'Model' 'Collection'
  808.             ),
  809.             $arrOptions
  810.         );
  811.         return static::find($arrOptions);
  812.     }
  813.     /**
  814.      * Find all records
  815.      *
  816.      * @param array $arrOptions An optional options array
  817.      *
  818.      * @return Collection|null The model collection or null if the result is empty
  819.      */
  820.     public static function findAll(array $arrOptions=array())
  821.     {
  822.         $arrOptions array_merge
  823.         (
  824.             array
  825.             (
  826.                 'return' => 'Collection'
  827.             ),
  828.             $arrOptions
  829.         );
  830.         return static::find($arrOptions);
  831.     }
  832.     /**
  833.      * Magic method to map Model::findByName() to Model::findBy('name')
  834.      *
  835.      * @param string $name The method name
  836.      * @param array  $args The passed arguments
  837.      *
  838.      * @return static|Collection|integer|null A model or model collection
  839.      *
  840.      * @throws \Exception If the method name is invalid
  841.      */
  842.     public static function __callStatic($name$args)
  843.     {
  844.         if (strncmp($name'findBy'6) === 0)
  845.         {
  846.             array_unshift($argslcfirst(substr($name6)));
  847.             return static::findBy(...$args);
  848.         }
  849.         if (strncmp($name'findOneBy'9) === 0)
  850.         {
  851.             array_unshift($argslcfirst(substr($name9)));
  852.             return static::findOneBy(...$args);
  853.         }
  854.         if (strncmp($name'countBy'7) === 0)
  855.         {
  856.             array_unshift($argslcfirst(substr($name7)));
  857.             return static::countBy(...$args);
  858.         }
  859.         throw new \Exception("Unknown method $name");
  860.     }
  861.     /**
  862.      * Find records and return the model or model collection
  863.      *
  864.      * Supported options:
  865.      *
  866.      * * column: the field name
  867.      * * value:  the field value
  868.      * * limit:  the maximum number of rows
  869.      * * offset: the number of rows to skip
  870.      * * order:  the sorting order
  871.      * * eager:  load all related records eagerly
  872.      *
  873.      * @param array $arrOptions The options array
  874.      *
  875.      * @return Model|Model[]|Collection|null A model, model collection or null if the result is empty
  876.      */
  877.     protected static function find(array $arrOptions)
  878.     {
  879.         if (!static::$strTable)
  880.         {
  881.             return null;
  882.         }
  883.         // Try to load from the registry
  884.         if ($arrOptions['return'] == 'Model')
  885.         {
  886.             $arrColumn = (array) $arrOptions['column'];
  887.             if (\count($arrColumn) == 1)
  888.             {
  889.                 // Support table prefixes
  890.                 $arrColumn[0] = preg_replace('/^' preg_quote(static::getTable(), '/') . '\./'''$arrColumn[0]);
  891.                 if ($arrColumn[0] == static::$strPk || \in_array($arrColumn[0], static::getUniqueFields()))
  892.                 {
  893.                     $varKey = \is_array($arrOptions['value']) ? $arrOptions['value'][0] : $arrOptions['value'];
  894.                     $objModel Registry::getInstance()->fetch(static::$strTable$varKey$arrColumn[0]);
  895.                     if ($objModel !== null)
  896.                     {
  897.                         return $objModel;
  898.                     }
  899.                 }
  900.             }
  901.         }
  902.         $arrOptions['table'] = static::$strTable;
  903.         $strQuery = static::buildFindQuery($arrOptions);
  904.         $objStatement Database::getInstance()->prepare($strQuery);
  905.         // Defaults for limit and offset
  906.         if (!isset($arrOptions['limit']))
  907.         {
  908.             $arrOptions['limit'] = 0;
  909.         }
  910.         if (!isset($arrOptions['offset']))
  911.         {
  912.             $arrOptions['offset'] = 0;
  913.         }
  914.         // Limit
  915.         if ($arrOptions['limit'] > || $arrOptions['offset'] > 0)
  916.         {
  917.             $objStatement->limit($arrOptions['limit'], $arrOptions['offset']);
  918.         }
  919.         $objStatement = static::preFind($objStatement);
  920.         $objResult $objStatement->execute($arrOptions['value']);
  921.         if ($objResult->numRows 1)
  922.         {
  923.             return $arrOptions['return'] == 'Array' ? array() : null;
  924.         }
  925.         $objResult = static::postFind($objResult);
  926.         // Try to load from the registry
  927.         if ($arrOptions['return'] == 'Model')
  928.         {
  929.             $objModel Registry::getInstance()->fetch(static::$strTable$objResult->{static::$strPk});
  930.             if ($objModel !== null)
  931.             {
  932.                 return $objModel->mergeRow($objResult->row());
  933.             }
  934.             return static::createModelFromDbResult($objResult);
  935.         }
  936.         if ($arrOptions['return'] == 'Array')
  937.         {
  938.             return static::createCollectionFromDbResult($objResult, static::$strTable)->getModels();
  939.         }
  940.         return static::createCollectionFromDbResult($objResult, static::$strTable);
  941.     }
  942.     /**
  943.      * Modify the database statement before it is executed
  944.      *
  945.      * @param Statement $objStatement The database statement object
  946.      *
  947.      * @return Statement The database statement object
  948.      */
  949.     protected static function preFind(Statement $objStatement)
  950.     {
  951.         return $objStatement;
  952.     }
  953.     /**
  954.      * Modify the database result before the model is created
  955.      *
  956.      * @param Result $objResult The database result object
  957.      *
  958.      * @return Result The database result object
  959.      */
  960.     protected static function postFind(Result $objResult)
  961.     {
  962.         return $objResult;
  963.     }
  964.     /**
  965.      * Return the number of records matching certain criteria
  966.      *
  967.      * @param mixed $strColumn  An optional property name
  968.      * @param mixed $varValue   An optional property value
  969.      * @param array $arrOptions An optional options array
  970.      *
  971.      * @return integer The number of matching rows
  972.      */
  973.     public static function countBy($strColumn=null$varValue=null, array $arrOptions=array())
  974.     {
  975.         if (!static::$strTable)
  976.         {
  977.             return 0;
  978.         }
  979.         $arrOptions array_merge
  980.         (
  981.             array
  982.             (
  983.                 'table'  => static::$strTable,
  984.                 'column' => $strColumn,
  985.                 'value'  => $varValue
  986.             ),
  987.             $arrOptions
  988.         );
  989.         $strQuery = static::buildCountQuery($arrOptions);
  990.         return (int) Database::getInstance()->prepare($strQuery)->execute($arrOptions['value'])->count;
  991.     }
  992.     /**
  993.      * Return the total number of rows
  994.      *
  995.      * @return integer The total number of rows
  996.      */
  997.     public static function countAll()
  998.     {
  999.         return static::countBy();
  1000.     }
  1001.     /**
  1002.      * Compile a Model class name from a table name (e.g. tl_form_field becomes FormFieldModel)
  1003.      *
  1004.      * @param string $strTable The table name
  1005.      *
  1006.      * @return string The model class name
  1007.      */
  1008.     public static function getClassFromTable($strTable)
  1009.     {
  1010.         if (isset(static::$arrClassNames[$strTable]))
  1011.         {
  1012.             return static::$arrClassNames[$strTable];
  1013.         }
  1014.         if (isset($GLOBALS['TL_MODELS'][$strTable]))
  1015.         {
  1016.             static::$arrClassNames[$strTable] = $GLOBALS['TL_MODELS'][$strTable]; // see 4796
  1017.             return static::$arrClassNames[$strTable];
  1018.         }
  1019.         $arrChunks explode('_'$strTable);
  1020.         if ($arrChunks[0] == 'tl')
  1021.         {
  1022.             array_shift($arrChunks);
  1023.         }
  1024.         static::$arrClassNames[$strTable] = implode(''array_map('ucfirst'$arrChunks)) . 'Model';
  1025.         return static::$arrClassNames[$strTable];
  1026.     }
  1027.     /**
  1028.      * Build a query based on the given options
  1029.      *
  1030.      * @param array $arrOptions The options array
  1031.      *
  1032.      * @return string The query string
  1033.      */
  1034.     protected static function buildFindQuery(array $arrOptions)
  1035.     {
  1036.         return QueryBuilder::find($arrOptions);
  1037.     }
  1038.     /**
  1039.      * Build a query based on the given options to count the number of records
  1040.      *
  1041.      * @param array $arrOptions The options array
  1042.      *
  1043.      * @return string The query string
  1044.      */
  1045.     protected static function buildCountQuery(array $arrOptions)
  1046.     {
  1047.         return QueryBuilder::count($arrOptions);
  1048.     }
  1049.     /**
  1050.      * Create a model from a database result
  1051.      *
  1052.      * @param Result $objResult The database result object
  1053.      *
  1054.      * @return static The model
  1055.      */
  1056.     protected static function createModelFromDbResult(Result $objResult)
  1057.     {
  1058.         /** @var static $strClass */
  1059.         $strClass = static::getClassFromTable(static::$strTable);
  1060.         return new $strClass($objResult);
  1061.     }
  1062.     /**
  1063.      * Create a Collection object
  1064.      *
  1065.      * @param array  $arrModels An array of models
  1066.      * @param string $strTable  The table name
  1067.      *
  1068.      * @return Collection The Collection object
  1069.      */
  1070.     protected static function createCollection(array $arrModels$strTable)
  1071.     {
  1072.         return new Collection($arrModels$strTable);
  1073.     }
  1074.     /**
  1075.      * Create a new collection from a database result
  1076.      *
  1077.      * @param Result $objResult The database result object
  1078.      * @param string $strTable  The table name
  1079.      *
  1080.      * @return Collection The model collection
  1081.      */
  1082.     protected static function createCollectionFromDbResult(Result $objResult$strTable)
  1083.     {
  1084.         return Collection::createFromDbResult($objResult$strTable);
  1085.     }
  1086.     /**
  1087.      * Check if the preview mode is enabled
  1088.      *
  1089.      * @param array $arrOptions The options array
  1090.      *
  1091.      * @return boolean
  1092.      */
  1093.     protected static function isPreviewMode(array $arrOptions)
  1094.     {
  1095.         if (isset($arrOptions['ignoreFePreview']))
  1096.         {
  1097.             return false;
  1098.         }
  1099.         return \defined('BE_USER_LOGGED_IN') && BE_USER_LOGGED_IN === true;
  1100.     }
  1101. }
  1102. class_alias(Model::class, 'Model');