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

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