diff --git a/system/Model.php b/system/Model.php index ec7365b9c3b0..4814217f7c01 100644 --- a/system/Model.php +++ b/system/Model.php @@ -1439,10 +1439,6 @@ public function validate($data): bool return true; } - // Replace any placeholders (i.e. {id}) in the rules with - // the value found in $data, if exists. - $rules = $this->fillPlaceholders($rules, $data); - $this->validation->setRules($rules, $this->validationMessages); $valid = $this->validation->run($data, null, $this->DBGroup); @@ -1495,6 +1491,10 @@ protected function cleanValidationRules(array $rules, array $data = null): array * * 'required|is_unique[users,email,id,13]' * + * @codeCoverageIgnore + * + * @deprecated use fillPlaceholders($rules, $data) from Validation instead + * * @param array $rules * @param array $data * diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 83eb3dd5ae7f..e42560834017 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -155,6 +155,10 @@ public function run(array $data = null, string $group = null, string $db_group = return false; } + // Replace any placeholders (i.e. {id}) in the rules with + // the value found in $data, if exists. + $this->rules = $this->fillPlaceholders($this->rules, $data); + // Need this for searching arrays in validation. helper('array'); @@ -603,6 +607,64 @@ public function loadRuleGroup(string $group = null) return $this->rules; } + //-------------------------------------------------------------------- + + /** + * Replace any placeholders within the rules with the values that + * match the 'key' of any properties being set. For example, if + * we had the following $data array: + * + * [ 'id' => 13 ] + * + * and the following rule: + * + * 'required|is_unique[users,email,id,{id}]' + * + * The value of {id} would be replaced with the actual id in the form data: + * + * 'required|is_unique[users,email,id,13]' + * + * @param array $rules + * @param array $data + * + * @return array + */ + protected function fillPlaceholders(array $rules, array $data): array + { + $replacements = []; + + foreach ($data as $key => $value) + { + $replacements["{{$key}}"] = $value; + } + + if (! empty($replacements)) + { + foreach ($rules as &$rule) + { + if (is_array($rule)) + { + foreach ($rule as &$row) + { + // Should only be an `errors` array + // which doesn't take placeholders. + if (is_array($row)) + { + continue; + } + + $row = strtr($row, $replacements); + } + continue; + } + + $rule = strtr($rule, $replacements); + } + } + + return $rules; + } + //-------------------------------------------------------------------- //-------------------------------------------------------------------- // Errors diff --git a/tests/system/Validation/UniqueRulesTest.php b/tests/system/Validation/UniqueRulesTest.php index f4564870de35..1a1af678140f 100644 --- a/tests/system/Validation/UniqueRulesTest.php +++ b/tests/system/Validation/UniqueRulesTest.php @@ -112,4 +112,35 @@ public function testIsUniqueIgnoresParams() } //-------------------------------------------------------------------- + + /** + * @group DatabaseLive + */ + public function testIsUniqueIgnoresParamsPlaceholders() + { + $this->hasInDatabase('user', [ + 'name' => 'Derek', + 'email' => 'derek@world.co.uk', + 'country' => 'GB', + ]); + + $db = Database::connect(); + $row = $db->table('user') + ->limit(1) + ->get() + ->getRow(); + + $data = [ + 'id' => $row->id, + 'email' => 'derek@world.co.uk', + ]; + + $this->validation->setRules([ + 'email' => "is_unique[user.email,id,{id}]", + ]); + + $this->assertTrue($this->validation->run($data)); + } + + //-------------------------------------------------------------------- } diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 389fdc275b42..c354e59527df 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -64,11 +64,11 @@ The Form Using a text editor, create a form called **Signup.php**. In it, place this code and save it to your **app/Views/** folder:: - -
-= anchor('form', 'Try it again!') ?>
- - + + The Controller ================================================ @@ -118,43 +118,43 @@ The Controller Using a text editor, create a controller called **Form.php**. In it, place this code and save it to your **app/Controllers/** folder:: - validate([])) - { - echo view('Signup', [ - 'validation' => $this->validator - ]); - } - else - { - echo view('Success'); - } - } - } + validate([])) + { + echo view('Signup', [ + 'validation' => $this->validator + ]); + } + else + { + echo view('Success'); + } + } + } Try it! ================================================ To try your form, visit your site using a URL similar to this one:: - example.com/index.php/form/ + example.com/index.php/form/ If you submit the form you should simply see the form reload. That's because you haven't set up any validation rules yet. -**Since you haven't told the Validation class to validate anything -yet, it returns false (boolean false) by default. The** ``validate()`` **method -only returns true if it has successfully applied your rules without any -of them failing.** +.. note:: Since you haven't told the **Validation class** to validate anything + yet, it **returns false** (boolean false) **by default**. The ``validate()`` + method only returns true if it has successfully applied your rules without + any of them failing. Explanation ================================================ @@ -171,7 +171,7 @@ The form (Signup.php) is a standard web form with a couple of exceptions: #. At the top of the form you'll notice the following function call: :: - = $validation->listErrors() ?> + = $validation->listErrors() ?> This function will return any error messages sent back by the validator. If there are no messages it returns an empty string. @@ -364,19 +364,17 @@ Or pass all settings in an array:: { public $signup = [ 'username' => [ - 'label' => 'Username', 'rules' => 'required', 'errors' => [ - 'required' => 'You must choose a {field}.' + 'required' => 'You must choose a Username.' + ] + ], + 'email' => [ + 'rules' => 'required|valid_email', + 'errors' => [ + 'valid_email' => 'Please check the Email field. It does not appear to be valid.' ] ], - 'email' => 'required|valid_email' - ]; - - public $signup_errors = [ - 'email' => [ - 'valid_email' => 'Please check the Email field. It does not appear to be valid.' - ] ]; } @@ -417,6 +415,37 @@ you previously set, so ``setRules()``, ``setRuleGroup()`` etc. need to be repeat } } +Validation Placeholders +======================================================= + +The Validation class provides a simple method to replace parts of your rules based on data that's being passed into it. This +sounds fairly obscure but can be especially handy with the ``is_unique`` validation rule. Placeholders are simply +the name of the field (or array key) that was passed in as $data surrounded by curly brackets. It will be +replaced by the **value** of the matched incoming field. An example should clarify this:: + + $validation->setRules([ + 'email' => 'required|valid_email|is_unique[users.email,id,{id}]' + ]); + +In this set of rules, it states that the email address should be unique in the database, except for the row +that has an id matching the placeholder's value. Assuming that the form POST data had the following:: + + $_POST = [ + 'id' => 4, + 'email' => 'foo@example.com' + ]; + +then the ``{id}`` placeholder would be replaced with the number **4**, giving this revised rule:: + + $validation->setRules([ + 'email' => 'required|valid_email|is_unique[users.email,id,4]' + ]); + +So it will ignore the row in the database that has ``id=4`` when it verifies the email is unique. + +This can also be used to create more dynamic rules at runtime, as long as you take care that any dynamic +keys passed in don't conflict with your form data. + Working With Errors ************************************************ @@ -487,7 +516,9 @@ at least 6 characters.” Translation Of Messages And Validation Labels ============================================= -To use translated strings from language files, we can simply use the dot syntax. Let's say we have a file with translations located here: ``app/Languages/en/Rules.php``. We can simply use the language lines defined in this file, like this:: +To use translated strings from language files, we can simply use the dot syntax. +Let's say we have a file with translations located here: ``app/Languages/en/Rules.php``. +We can simply use the language lines defined in this file, like this:: $validation->setRules([ 'username' => [ @@ -611,10 +642,10 @@ autoloader can find it. These files are called RuleSets. To add a new RuleSet, e add the new file to the ``$ruleSets`` array:: public $ruleSets = [ - \CodeIgniter\Validation\Rules::class, - \CodeIgniter\Validation\FileRules::class, - \CodeIgniter\Validation\CreditCardRules::class, - ]; + \CodeIgniter\Validation\Rules::class, + \CodeIgniter\Validation\FileRules::class, + \CodeIgniter\Validation\CreditCardRules::class, + ]; You can add it as either a simple string with the fully qualified class name, or using the ``::class`` suffix as shown above. The primary benefit here is that it provides some extra navigation capabilities in more advanced IDEs. @@ -658,41 +689,41 @@ If your method needs to work with parameters, the function will need a minimum o the parameter string, and an array with all of the data that was submitted the form. The $data array is especially handy for rules like ``require_with`` that needs to check the value of another submitted field to base its result on:: - public function required_with($str, string $fields, array $data): bool - { - $fields = explode(',', $fields); + public function required_with($str, string $fields, array $data): bool + { + $fields = explode(',', $fields); - // If the field is present we can safely assume that - // the field is here, no matter whether the corresponding - // search field is present or not. - $present = $this->required($str ?? ''); + // If the field is present we can safely assume that + // the field is here, no matter whether the corresponding + // search field is present or not. + $present = $this->required($str ?? ''); - if ($present) - { - return true; - } + if ($present) + { + return true; + } // Still here? Then we fail this test if - // any of the fields are present in $data - // as $fields is the lis - $requiredFields = []; - - foreach ($fields as $field) - { - if (array_key_exists($field, $data)) - { - $requiredFields[] = $field; - } - } - - // Remove any keys with empty values since, that means they - // weren't truly there, as far as this is concerned. - $requiredFields = array_filter($requiredFields, function ($item) use ($data) { - return ! empty($data[$item]); - }); - - return empty($requiredFields); - } + // any of the fields are present in $data + // as $fields is the lis + $requiredFields = []; + + foreach ($fields as $field) + { + if (array_key_exists($field, $data)) + { + $requiredFields[] = $field; + } + } + + // Remove any keys with empty values since, that means they + // weren't truly there, as far as this is concerned. + $requiredFields = array_filter($requiredFields, function ($item) use ($data) { + return ! empty($data[$item]); + }); + + return empty($requiredFields); + } Custom errors can be returned as the fourth parameter, just as described above. @@ -701,12 +732,20 @@ Available Rules The following is a list of all the native rules that are available to use: -.. note:: Rule is a string; there must be no spaces between the parameters, especially the "is_unique" rule. - There can be no spaces before and after "ignore_value". +.. note:: Rule is a string; there must be **no spaces** between the parameters, especially the ``is_unique`` rule. + There can be no spaces before and after ``ignore_value``. + +:: + + // is_unique[table.field,ignore_field,ignore_value] + + $validation->setRules([ + 'name' => "is_unique[supplier.name,uuid, $uuid]", // is not ok + 'name' => "is_unique[supplier.name,uuid,$uuid ]", // is not ok + 'name' => "is_unique[supplier.name,uuid,$uuid]", // is ok + 'name' => "is_unique[supplier.name,uuid,{uuid}]", // is ok - see "Validation Placeholders" + ]); -- "is_unique[supplier.name,uuid, $uuid]" is not ok -- "is_unique[supplier.name,uuid,$uuid ]" is not ok -- "is_unique[supplier.name,uuid,$uuid]" is ok ======================= =========== =============================================================================================== =================================================== Rule Parameter Description Example @@ -802,5 +841,5 @@ is_image Yes Fails if the file cannot be determined to be The file validation rules apply for both single and multiple file uploads. .. note:: You can also use any native PHP functions that permit up - to two parameters, where at least one is required (to pass - the field data). + to two parameters, where at least one is required (to pass + the field data). diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index c838a5a47475..9d80b03c74a7 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -24,7 +24,7 @@ You can access models within your classes by creating a new instance or using th :: // Create a new class manually - $userModel = new App\Models\UserModel(); + $userModel = new \App\Models\UserModel(); // Create a new class with the model function $userModel = model('App\Models\UserModel', false); @@ -59,14 +59,14 @@ Creating Your Model To take advantage of CodeIgniter's model, you would simply create a new model class that extends ``CodeIgniter\Model``:: - find($user_id); + $user = $userModel->find($user_id); The value is returned in the format specified in $returnType. You can specify more than one row to return by passing an array of primaryKey values instead of just one:: - $users = $userModel->find([1,2,3]); + $users = $userModel->find([1,2,3]); If no parameters are passed in, will return all rows in that model's table, effectively acting like findAll(), though less explicit. **findColumn()** - Returns null or an indexed array of column values:: +Returns null or an indexed array of column values:: - $user = $userModel->findColumn($column_name); + $user = $userModel->findColumn($column_name); - $column_name should be a name of single column else you will get the DataException. +$column_name should be a name of single column else you will get the DataException. **findAll()** Returns all results:: - $users = $userModel->findAll(); + $users = $userModel->findAll(); This query may be modified by interjecting Query Builder commands as needed prior to calling this method:: - $users = $userModel->where('active', 1) - ->findAll(); + $users = $userModel->where('active', 1) + ->findAll(); You can pass in a limit and offset values as the first and second parameters, respectively:: - $users = $userModel->findAll($limit, $offset); + $users = $userModel->findAll($limit, $offset); **first()** Returns the first row in the result set. This is best used in combination with the query builder. :: - $user = $userModel->where('deleted', 0) - ->first(); + $user = $userModel->where('deleted', 0) + ->first(); **withDeleted()** @@ -281,20 +281,20 @@ If $useSoftDeletes is true, then the find* methods will not return any rows wher To temporarily override this, you can use the withDeleted() method prior to calling the find* method. :: - // Only gets non-deleted rows (deleted = 0) - $activeUsers = $userModel->findAll(); + // Only gets non-deleted rows (deleted = 0) + $activeUsers = $userModel->findAll(); - // Gets all rows - $allUsers = $userModel->withDeleted() - ->findAll(); + // Gets all rows + $allUsers = $userModel->withDeleted() + ->findAll(); **onlyDeleted()** Whereas withDeleted() will return both deleted and not-deleted rows, this method modifies the next find* methods to return only soft deleted rows:: - $deletedUsers = $userModel->onlyDeleted() - ->findAll(); + $deletedUsers = $userModel->onlyDeleted() + ->findAll(); Saving Data ----------- @@ -305,12 +305,12 @@ An associative array of data is passed into this method as the only parameter to row of data in the database. The array's keys must match the name of the columns in a $table, while the array's values are the values to save for that key:: - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; + $data = [ + 'username' => 'darth', + 'email' => 'd.vader@theempire.com' + ]; - $userModel->insert($data); + $userModel->insert($data); **update()** @@ -318,20 +318,20 @@ Updates an existing record in the database. The first parameter is the $primaryK An associative array of data is passed into this method as the second parameter. The array's keys must match the name of the columns in a $table, while the array's values are the values to save for that key:: - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; + $data = [ + 'username' => 'darth', + 'email' => 'd.vader@theempire.com' + ]; - $userModel->update($id, $data); + $userModel->update($id, $data); Multiple records may be updated with a single call by passing an array of primary keys as the first parameter:: $data = [ - 'active' => 1 - ]; + 'active' => 1 + ]; - $userModel->update([1, 2, 3], $data); + $userModel->update([1, 2, 3], $data); When you need a more flexible solution, you can leave the parameters empty and it functions like the Query Builder's update command, with the added benefit of validation, events, etc:: @@ -346,24 +346,24 @@ update command, with the added benefit of validation, events, etc:: This is a wrapper around the insert() and update() methods that handle inserting or updating the record automatically, based on whether it finds an array key matching the $primaryKey value:: - // Defined as a model property - $primaryKey = 'id'; + // Defined as a model property + $primaryKey = 'id'; - // Does an insert() - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; + // Does an insert() + $data = [ + 'username' => 'darth', + 'email' => 'd.vader@theempire.com' + ]; - $userModel->save($data); + $userModel->save($data); - // Performs an update, since the primary key, 'id', is found. - $data = [ - 'id' => 3, - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; - $userModel->save($data); + // Performs an update, since the primary key, 'id', is found. + $data = [ + 'id' => 3, + 'username' => 'darth', + 'email' => 'd.vader@theempire.com' + ]; + $userModel->save($data); The save method also can make working with custom class result objects much simpler by recognizing a non-simple object and grabbing its public and protected values into an array, which is then passed to the appropriate @@ -373,59 +373,59 @@ class is responsible for maintaining the business logic surrounding the object i elements in a certain way, etc. They shouldn't have any idea about how they are saved to the database. At their simplest, they might look like this:: - namespace App\Entities; - - class Job - { - protected $id; - protected $name; - protected $description; - - public function __get($key) - { - if (property_exists($this, $key)) - { - return $this->$key; - } - } - - public function __set($key, $value) - { - if (property_exists($this, $key)) - { - $this->$key = $value; - } - } - } + namespace App\Entities; + + class Job + { + protected $id; + protected $name; + protected $description; + + public function __get($key) + { + if (property_exists($this, $key)) + { + return $this->$key; + } + } + + public function __set($key, $value) + { + if (property_exists($this, $key)) + { + $this->$key = $value; + } + } + } A very simple model to work with this might look like:: - use CodeIgniter\Model; + use CodeIgniter\Model; - class JobModel extends Model - { - protected $table = 'jobs'; - protected $returnType = '\App\Entities\Job'; - protected $allowedFields = [ - 'name', 'description' - ]; - } + class JobModel extends Model + { + protected $table = 'jobs'; + protected $returnType = '\App\Entities\Job'; + protected $allowedFields = [ + 'name', 'description' + ]; + } This model works with data from the ``jobs`` table, and returns all results as an instance of ``App\Entities\Job``. When you need to persist that record to the database, you will need to either write custom methods, or use the model's ``save()`` method to inspect the class, grab any public and private properties, and save them to the database:: - // Retrieve a Job instance - $job = $model->find(15); + // Retrieve a Job instance + $job = $model->find(15); - // Make some changes - $job->name = "Foobar"; + // Make some changes + $job->name = "Foobar"; - // Save the changes - $model->save($job); + // Save the changes + $model->save($job); .. note:: If you find yourself working with Entities a lot, CodeIgniter provides a built-in :doc:`Entity class ` - that provides several handy features that make developing Entities simpler. + that provides several handy features that make developing Entities simpler. Deleting Data ------------- @@ -434,7 +434,7 @@ Deleting Data Takes a primary key value as the first parameter and deletes the matching record from the model's table:: - $userModel->delete(12); + $userModel->delete(12); If the model's $useSoftDeletes value is true, this will update the row to set ``deleted_at`` to the current date and time. You can force a permanent delete by setting the second parameter as true. @@ -452,7 +452,7 @@ previously:: Cleans out the database table by permanently removing all rows that have 'deleted_at IS NOT NULL'. :: - $userModel->purgeDeleted(); + $userModel->purgeDeleted(); Validating Data --------------- @@ -464,81 +464,81 @@ prior to saving to the database with the ``insert()``, ``update()``, or ``save() The first step is to fill out the ``$validationRules`` class property with the fields and rules that should be applied. If you have custom error message that you want to use, place them in the ``$validationMessages`` array:: - class UserModel extends Model - { - protected $validationRules = [ - 'username' => 'required|alpha_numeric_space|min_length[3]', - 'email' => 'required|valid_email|is_unique[users.email]', - 'password' => 'required|min_length[8]', - 'pass_confirm' => 'required_with[password]|matches[password]' - ]; - - protected $validationMessages = [ - 'email' => [ - 'is_unique' => 'Sorry. That email has already been taken. Please choose another.' - ] - ]; - } + class UserModel extends Model + { + protected $validationRules = [ + 'username' => 'required|alpha_numeric_space|min_length[3]', + 'email' => 'required|valid_email|is_unique[users.email]', + 'password' => 'required|min_length[8]', + 'pass_confirm' => 'required_with[password]|matches[password]' + ]; + + protected $validationMessages = [ + 'email' => [ + 'is_unique' => 'Sorry. That email has already been taken. Please choose another.' + ] + ]; + } The other way to set the validation message to fields by functions, .. php:function:: setValidationMessage($field, $fieldMessages) - :param string $field - :param array $fieldMessages + :param string $field: + :param array $fieldMessages: - This function will set the field wise error messages. + This function will set the field wise error messages. - Usage example:: + Usage example:: - $fieldName = 'name'; - $fieldValidationMessage = array( - 'required' => 'Your name is required here', - ); - $model->setValidationMessage($fieldName, $fieldValidationMessage); + $fieldName = 'name'; + $fieldValidationMessage = [ + 'required' => 'Your name is required here', + ]; + $model->setValidationMessage($fieldName, $fieldValidationMessage); .. php:function:: setValidationMessages($fieldMessages) - :param array $fieldMessages + :param array $fieldMessages: - This function will set the field messages. + This function will set the field messages. - Usage example:: + Usage example:: - $fieldValidationMessage = array( - 'name' => array( - 'required' => 'Your baby name is missing.', - 'min_length' => 'Too short, man!', - ), - ); - $model->setValidationMessages($fieldValidationMessage); + $fieldValidationMessage = [ + 'name' => [ + 'required' => 'Your baby name is missing.', + 'min_length' => 'Too short, man!', + ], + ]; + $model->setValidationMessages($fieldValidationMessage); Now, whenever you call the ``insert()``, ``update()``, or ``save()`` methods, the data will be validated. If it fails, the model will return boolean **false**. You can use the ``errors()`` method to retrieve the validation errors:: - if ($model->save($data) === false) - { - return view('updateUser', ['errors' => $model->errors()]; - } + if ($model->save($data) === false) + { + return view('updateUser', ['errors' => $model->errors()]; + } This returns an array with the field names and their associated errors that can be used to either show all of the errors at the top of the form, or to display them individually:: - -= $error ?>
- -= $error ?>
+ +