Merge remote-tracking branch 'upstream/develop' into 4.4

Conflicts:
	phpstan-baseline.neon.dist
This commit is contained in:
kenjis 2023-07-16 15:00:59 +09:00
commit 50c225c54b
No known key found for this signature in database
GPG Key ID: BD254878922AF198
40 changed files with 923 additions and 177 deletions

View File

@ -13,7 +13,10 @@ use CodeIgniter\Config\AutoloadConfig;
* can find the files as needed.
*
* NOTE: If you use an identical key in $psr4 or $classmap, then
* the values in this file will overwrite the framework's values.
* the values in this file will overwrite the framework's values.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/
class Autoload extends AutoloadConfig
{

View File

@ -95,7 +95,8 @@ class Cache extends BaseConfig
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
* Note: The default set is required for PSR-6 compliance.
*
* NOTE: The default set is required for PSR-6 compliance.
*/
public string $reservedCharacters = '{}()/\@:';

View File

@ -39,7 +39,7 @@ class ContentSecurityPolicy extends BaseConfig
// -------------------------------------------------------------------------
// Sources allowed
// Note: once you set a policy to 'none', it cannot be further restricted
// NOTE: once you set a policy to 'none', it cannot be further restricted
// -------------------------------------------------------------------------
/**

View File

@ -99,7 +99,7 @@ class Logger extends BaseConfig
* An extension of 'php' allows for protecting the log files via basic
* scripting, when they are to be stored under a publicly accessible directory.
*
* Note: Leaving it blank will default to 'log'.
* NOTE: Leaving it blank will default to 'log'.
*/
'fileExtension' => '',

View File

@ -40,7 +40,7 @@ class Migrations extends BaseConfig
* using the CLI command:
* > php spark make:migration
*
* Note: if you set an unsupported format, migration runner will not find
* NOTE: if you set an unsupported format, migration runner will not find
* your migration files.
*
* Supported formats:

View File

@ -4,6 +4,12 @@ namespace Config;
use CodeIgniter\Modules\Modules as BaseModules;
/**
* Modules Configuration.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/
class Modules extends BaseModules
{
/**

View File

@ -12,6 +12,11 @@ namespace Config;
* share a system folder between multiple applications, and more.
*
* All paths are relative to the project's root folder.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Paths
{

View File

@ -24,7 +24,7 @@
"phpunit/phpcov": "^8.2",
"phpunit/phpunit": "^9.1",
"predis/predis": "^1.1 || ^2.0",
"rector/rector": "0.17.3",
"rector/rector": "0.17.6",
"vimeo/psalm": "^5.0"
},
"suggest": {

View File

@ -10,11 +10,6 @@ parameters:
count: 1
path: system/Autoloader/Autoloader.php
-
message: "#^Property CodeIgniter\\\\Cache\\\\Handlers\\\\RedisHandler\\:\\:\\$redis \\(Redis\\) in isset\\(\\) is not nullable\\.$#"
count: 1
path: system/Cache/Handlers/RedisHandler.php
-
message: "#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$db \\(CodeIgniter\\\\Database\\\\BaseConnection\\) in empty\\(\\) is not falsy\\.$#"
count: 1
@ -192,16 +187,11 @@ parameters:
-
message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(.*\\)\\.$#"
count: 2
path: system/Router/Router.php
-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
path: system/Router/Router.php
-
message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#"
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
path: system/Router/Router.php

View File

@ -1345,7 +1345,7 @@ abstract class BaseModel
}
/**
* Allows to set validation messages.
* Allows to set (and reset) validation messages.
* It could be used when you have to change default or override current validate messages.
*
* @param array $validationMessages Value
@ -1376,7 +1376,7 @@ abstract class BaseModel
}
/**
* Allows to set validation rules.
* Allows to set (and reset) validation rules.
* It could be used when you have to change default or override current validate rules.
*
* @param array $validationRules Value
@ -1401,6 +1401,17 @@ abstract class BaseModel
*/
public function setValidationRule(string $field, $fieldRules)
{
$rules = $this->validationRules;
// ValidationRules can be either a string, which is the group name,
// or an array of rules.
if (is_string($rules)) {
[$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
$this->validationRules = $rules;
$this->validationMessages = $this->validationMessages + $customErrors;
}
$this->validationRules[$field] = $fieldRules;
return $this;
@ -1466,7 +1477,9 @@ abstract class BaseModel
// ValidationRules can be either a string, which is the group name,
// or an array of rules.
if (is_string($rules)) {
$rules = $this->validation->loadRuleGroup($rules);
[$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
$this->validationMessages = $this->validationMessages + $customErrors;
}
if (isset($options['except'])) {

View File

@ -38,7 +38,7 @@ class RedisHandler extends BaseHandler
/**
* Redis connection
*
* @var Redis
* @var Redis|null
*/
protected $redis;

View File

@ -484,7 +484,7 @@ class Services extends BaseService
return static::getSharedInstance('parser', $viewPath, $config);
}
$viewPath = $viewPath ?: config(Paths::class)->viewDirectory;
$viewPath = $viewPath ?: (new Paths())->viewDirectory;
$config ??= config(ViewConfig::class);
return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
@ -503,7 +503,7 @@ class Services extends BaseService
return static::getSharedInstance('renderer', $viewPath, $config);
}
$viewPath = $viewPath ?: config(Paths::class)->viewDirectory;
$viewPath = $viewPath ?: (new Paths())->viewDirectory;
$config ??= config(ViewConfig::class);
return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());

View File

@ -69,7 +69,9 @@ if (! function_exists('number_to_amount')) {
*
* @see https://simple.wikipedia.org/wiki/Names_for_large_numbers
*
* @param int|string $num
* @param int|string $num Will be cast as int
* @param int $precision [optional] The optional number of decimal digits to round to.
* @param string|null $locale [optional]
*
* @return bool|string
*/
@ -91,19 +93,19 @@ if (! function_exists('number_to_amount')) {
$generalLocale = substr($locale, 0, $underscorePos);
}
if ($num > 1_000_000_000_000_000) {
if ($num >= 1_000_000_000_000_000) {
$suffix = lang('Number.quadrillion', [], $generalLocale);
$num = round(($num / 1_000_000_000_000_000), $precision);
} elseif ($num > 1_000_000_000_000) {
} elseif ($num >= 1_000_000_000_000) {
$suffix = lang('Number.trillion', [], $generalLocale);
$num = round(($num / 1_000_000_000_000), $precision);
} elseif ($num > 1_000_000_000) {
} elseif ($num >= 1_000_000_000) {
$suffix = lang('Number.billion', [], $generalLocale);
$num = round(($num / 1_000_000_000), $precision);
} elseif ($num > 1_000_000) {
} elseif ($num >= 1_000_000) {
$suffix = lang('Number.million', [], $generalLocale);
$num = round(($num / 1_000_000), $precision);
} elseif ($num > 1000) {
} elseif ($num >= 1000) {
$suffix = lang('Number.thousand', [], $generalLocale);
$num = round(($num / 1000), $precision);
}

View File

@ -11,6 +11,7 @@
namespace CodeIgniter\Router;
use Closure;
use CodeIgniter\Exceptions\PageNotFoundException;
/**
@ -19,11 +20,11 @@ use CodeIgniter\Exceptions\PageNotFoundException;
final class AutoRouter implements AutoRouterInterface
{
/**
* List of controllers registered for the CLI verb that should not be accessed in the web.
* List of CLI routes that do not contain '*' routes.
*
* @var class-string[]
* @var array<string, Closure|string> [routeKey => handler]
*/
private array $protectedControllers;
private array $cliRoutes;
/**
* Sub-directory that contains the requested controller class.
@ -58,17 +59,17 @@ final class AutoRouter implements AutoRouterInterface
private string $defaultNamespace;
public function __construct(
array $protectedControllers,
array $cliRoutes,
string $defaultNamespace,
string $defaultController,
string $defaultMethod,
bool $translateURIDashes,
string $httpVerb
) {
$this->protectedControllers = $protectedControllers;
$this->defaultNamespace = $defaultNamespace;
$this->translateURIDashes = $translateURIDashes;
$this->httpVerb = $httpVerb;
$this->cliRoutes = $cliRoutes;
$this->defaultNamespace = $defaultNamespace;
$this->translateURIDashes = $translateURIDashes;
$this->httpVerb = $httpVerb;
$this->controller = $defaultController;
$this->method = $defaultMethod;
@ -126,18 +127,31 @@ final class AutoRouter implements AutoRouterInterface
$controller .= $controllerName;
$controller = strtolower($controller);
$methodName = strtolower($this->methodName());
foreach ($this->protectedControllers as $controllerInRoute) {
if (! is_string($controllerInRoute)) {
continue;
}
if (strtolower($controllerInRoute) !== $controller) {
continue;
}
foreach ($this->cliRoutes as $handler) {
if (is_string($handler)) {
$handler = strtolower($handler);
throw new PageNotFoundException(
'Cannot access the controller in a CLI Route. Controller: ' . $controllerInRoute
);
// Like $routes->cli('hello/(:segment)', 'Home::$1')
if (strpos($handler, '::$') !== false) {
throw new PageNotFoundException(
'Cannot access CLI Route: ' . $uri
);
}
if (strpos($handler, $controller . '::' . $methodName) === 0) {
throw new PageNotFoundException(
'Cannot access CLI Route: ' . $uri
);
}
if ($handler === $controller) {
throw new PageNotFoundException(
'Cannot access CLI Route: ' . $uri
);
}
}
}
}

View File

@ -550,8 +550,10 @@ class RouteCollection implements RouteCollectionInterface
/**
* Returns the raw array of available routes.
*
* @param bool $includeWildcard Whether to include '*' routes.
*/
public function getRoutes(?string $verb = null): array
public function getRoutes(?string $verb = null, bool $includeWildcard = true): array
{
if (empty($verb)) {
$verb = $this->getHTTPVerb();
@ -567,7 +569,7 @@ class RouteCollection implements RouteCollectionInterface
if (isset($this->routes[$verb])) {
// Keep current verb's routes at the beginning, so they're matched
// before any of the generic, "add" routes.
$collection = $this->routes[$verb] + ($this->routes['*'] ?? []);
$collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
foreach ($collection as $routeKey => $r) {
$routes[$routeKey] = $r['handler'];

View File

@ -145,7 +145,7 @@ class Router implements RouterInterface
);
} else {
$this->autoRouter = new AutoRouter(
$this->collection->getRegisteredControllers('cli'),
$this->collection->getRoutes('cli', false), // @phpstan-ignore-line
$this->collection->getDefaultNamespace(),
$this->collection->getDefaultController(),
$this->collection->getDefaultMethod(),
@ -393,6 +393,7 @@ class Router implements RouterInterface
*/
protected function checkRoutes(string $uri): bool
{
// @phpstan-ignore-next-line
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
// Don't waste any time

View File

@ -696,7 +696,7 @@ class Validation implements ValidationInterface
* same format used with setRules(). Additionally, check
* for {group}_errors for an array of custom error messages.
*
* @return array
* @return array<int, array> [rules, customErrors]
*
* @throws ValidationException
*/
@ -724,7 +724,7 @@ class Validation implements ValidationInterface
$this->customErrors = $this->config->{$errorName};
}
return $this->rules;
return [$this->rules, $this->customErrors];
}
/**

View File

@ -178,7 +178,8 @@ class Cell
}
// locate and return an instance of the cell
$object = Factories::cells($class);
// @TODO extend Factories to be able to load classes with the same short name.
$object = class_exists($class) ? new $class() : Factories::cells($class);
if (! is_object($object)) {
throw ViewException::forInvalidCellClass($class);

View File

@ -0,0 +1,32 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Tests\Support\Config;
use Config\Validation as ValidationConfig;
class Validation extends ValidationConfig
{
public $signup = [
'id' => 'permit_empty|is_natural_no_zero',
'name' => [
'required',
'min_length[3]',
],
'token' => 'permit_empty|in_list[{id}]',
];
public $signup_errors = [
'name' => [
'required' => 'You forgot to name the baby.',
'min_length' => 'Too short, man!',
],
];
}

View File

@ -0,0 +1,27 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Tests\Support\Models;
use CodeIgniter\Model;
class ValidModelRuleGroup extends Model
{
protected $table = 'job';
protected $returnType = 'object';
protected $useSoftDeletes = false;
protected $dateFormat = 'int';
protected $allowedFields = [
'name',
'description',
];
protected $validationRules = 'signup';
}

View File

@ -0,0 +1,26 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Tests\Support\View\OtherCells;
/**
* Two classes with the same short name.
*
* - Tests\Support\View\SampleClass
* - Tests\Support\View\OtherCells\SampleClass
*/
class SampleClass
{
public function hello()
{
return 'Good-bye!';
}
}

View File

@ -101,26 +101,45 @@ final class NumberHelperTest extends CIUnitTestCase
public function testThousands()
{
$this->assertSame('123 thousand', number_to_amount('123,000', 0, 'en_US'));
$this->assertSame('1 thousand', number_to_amount('1000', 0, 'en_US'));
$this->assertSame('999 thousand', number_to_amount('999499', 0, 'en_US'));
$this->assertSame('1,000 thousand', number_to_amount('999500', 0, 'en_US'));
$this->assertSame('1,000 thousand', number_to_amount('999999', 0, 'en_US'));
}
public function testMillions()
{
$this->assertSame('123.4 million', number_to_amount('123,400,000', 1, 'en_US'));
$this->assertSame('1 million', number_to_amount('1,000,000', 1, 'en_US'));
$this->assertSame('1.5 million', number_to_amount('1,499,999', 1, 'en_US'));
$this->assertSame('1.5 million', number_to_amount('1,500,000', 1, 'en_US'));
$this->assertSame('1.5 million', number_to_amount('1,549,999', 1, 'en_US'));
$this->assertSame('1.6 million', number_to_amount('1,550,000', 1, 'en_US'));
$this->assertSame('999.5 million', number_to_amount('999,500,000', 1, 'en_US'));
$this->assertSame('1,000 million', number_to_amount('999,500,000', 0, 'en_US'));
$this->assertSame('1,000 million', number_to_amount('999,999,999', 1, 'en_US'));
}
public function testBillions()
{
$this->assertSame('123.46 billion', number_to_amount('123,456,000,000', 2, 'en_US'));
$this->assertSame('1 billion', number_to_amount('1,000,000,000', 2, 'en_US'));
$this->assertSame('1,000 billion', number_to_amount('999,999,999,999', 2, 'en_US'));
}
public function testTrillions()
{
$this->assertSame('123.457 trillion', number_to_amount('123,456,700,000,000', 3, 'en_US'));
$this->assertSame('1 trillion', number_to_amount('1,000,000,000,000', 3, 'en_US'));
$this->assertSame('1,000 trillion', number_to_amount('999,999,999,999,999', 3, 'en_US'));
}
public function testQuadrillions()
{
$this->assertSame('123.5 quadrillion', number_to_amount('123,456,700,000,000,000', 1, 'en_US'));
$this->assertSame('1 quadrillion', number_to_amount('1,000,000,000,000,000', 0, 'en_US'));
$this->assertSame('1,000 quadrillion', number_to_amount('999,999,999,999,999,999', 0, 'en_US'));
$this->assertSame('1,000 quadrillion', number_to_amount('1,000,000,000,000,000,000', 0, 'en_US'));
}
public function testCurrencyCurrentLocale()

View File

@ -0,0 +1,476 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Models;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Model;
use Config\Services;
use stdClass;
use Tests\Support\Config\Validation;
use Tests\Support\Models\JobModel;
use Tests\Support\Models\SimpleEntity;
use Tests\Support\Models\ValidErrorsModel;
use Tests\Support\Models\ValidModelRuleGroup;
/**
* @group DatabaseLive
*
* @internal
*/
final class ValidationModelRuleGroupTest extends LiveModelTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createModel(ValidModelRuleGroup::class);
}
protected function createModel(string $modelName, ?BaseConnection $db = null): Model
{
$config = new Validation();
$validation = new \CodeIgniter\Validation\Validation($config, Services::renderer());
$this->db = $db ?? $this->db;
$this->model = new $modelName($this->db, $validation);
return $this->model;
}
public function testValid(): void
{
$data = [
'name' => 'some name',
'description' => 'some great marketing stuff',
];
$this->assertIsInt($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame([], $errors);
}
public function testValidationBasics(): void
{
$data = [
'name' => null,
'description' => 'some great marketing stuff',
];
$this->assertFalse($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame('You forgot to name the baby.', $errors['name']);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5859
*/
public function testValidationTwice(): void
{
$data = [
'name' => null,
'description' => 'some great marketing stuff',
];
$this->assertFalse($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame('You forgot to name the baby.', $errors['name']);
$data = [
'name' => 'some name',
'description' => 'some great marketing stuff',
];
$this->assertIsInt($this->model->insert($data));
}
public function testValidationWithSetValidationRule(): void
{
$data = [
'name' => 'some name',
'description' => 'some great marketing stuff',
];
$this->model->setValidationRule('description', [
'rules' => 'required|min_length[50]',
'errors' => [
'min_length' => 'Description is too short baby.',
],
]);
$this->assertFalse($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame('Description is too short baby.', $errors['description']);
}
public function testValidationWithSetValidationRules(): void
{
$data = [
'name' => '',
'description' => 'some great marketing stuff',
];
$this->model->setValidationRules([
'name' => [
'rules' => 'required',
'errors' => [
'required' => 'Give me a name baby.',
],
],
'description' => [
'rules' => 'required|min_length[50]',
'errors' => [
'min_length' => 'Description is too short baby.',
],
],
]);
$this->assertFalse($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame('Give me a name baby.', $errors['name']);
$this->assertSame('Description is too short baby.', $errors['description']);
}
public function testValidationWithSetValidationMessage(): void
{
$data = [
'name' => null,
'description' => 'some great marketing stuff',
];
$this->model->setValidationMessage('name', [
'required' => 'Your baby name is missing.',
'min_length' => 'Too short, man!',
]);
$this->assertFalse($this->model->insert($data));
$errors = $this->model->errors();
$this->assertSame('Your baby name is missing.', $errors['name']);
}
public function testValidationPlaceholdersSuccess(): void
{
$data = [
'name' => 'abc',
'id' => 13,
'token' => 13,
];
$this->assertTrue($this->model->validate($data));
}
public function testValidationPlaceholdersFail(): void
{
$data = [
'name' => 'abc',
'id' => 13,
'token' => 12,
];
$this->assertFalse($this->model->validate($data));
}
public function testSkipValidation(): void
{
$data = [
'name' => '2',
'description' => 'some great marketing stuff',
];
$this->assertIsNumeric($this->model->skipValidation(true)->insert($data));
}
public function testCleanValidationRemovesAllWhenNoDataProvided(): void
{
$cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules');
$rules = [
'name' => 'required',
'foo' => 'bar',
];
$rules = $cleaner($rules, null);
$this->assertEmpty($rules);
}
public function testCleanValidationRemovesOnlyForFieldsNotProvided(): void
{
$cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules');
$rules = [
'name' => 'required',
'foo' => 'required',
];
$data = [
'foo' => 'bar',
];
$rules = $cleaner($rules, $data);
$this->assertArrayHasKey('foo', $rules);
$this->assertArrayNotHasKey('name', $rules);
}
public function testCleanValidationReturnsAllWhenAllExist(): void
{
$cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules');
$rules = [
'name' => 'required',
'foo' => 'required',
];
$data = [
'foo' => 'bar',
'name' => null,
];
$rules = $cleaner($rules, $data);
$this->assertArrayHasKey('foo', $rules);
$this->assertArrayHasKey('name', $rules);
}
public function testValidationPassesWithMissingFields(): void
{
$data = [
'foo' => 'bar',
];
$result = $this->model->validate($data);
$this->assertTrue($result);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1584
*/
public function testUpdateWithValidation(): void
{
$data = [
'description' => 'This is a first test!',
'name' => 'valid',
'id' => 42,
'token' => 42,
];
$id = $this->model->insert($data);
$this->assertTrue((bool) $id);
$data['description'] = 'This is a second test!';
unset($data['name']);
$result = $this->model->update($id, $data);
$this->assertTrue($result);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
*/
public function testRequiredWithValidationEmptyString(): void
{
$this->assertFalse($this->model->insert(['name' => '']));
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
*/
public function testRequiredWithValidationNull(): void
{
$this->assertFalse($this->model->insert(['name' => null]));
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
*/
public function testRequiredWithValidationTrue(): void
{
$data = [
'name' => 'foobar',
'description' => 'just because we have to',
];
$this->assertNotFalse($this->model->insert($data));
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1574
*/
public function testValidationIncludingErrors(): void
{
$data = [
'description' => 'This is a first test!',
'name' => 'valid',
'id' => 42,
'token' => 42,
];
$this->createModel(ValidErrorsModel::class);
$id = $this->model->insert($data);
$this->assertFalse((bool) $id);
$this->assertSame('Minimum Length Error', $this->model->errors()['name']);
}
public function testValidationByObject(): void
{
$data = new stdClass();
$data->name = 'abc';
$data->id = '13';
$data->token = '13';
$this->assertTrue($this->model->validate($data));
}
public function testGetValidationRules(): void
{
$this->createModel(JobModel::class);
$this->setPrivateProperty($this->model, 'validationRules', ['description' => 'required']);
$rules = $this->model->getValidationRules();
$this->assertSame('required', $rules['description']);
}
public function testGetValidationMessages(): void
{
$jobData = [
[
'name' => 'Comedian',
'description' => null,
],
];
$this->createModel(JobModel::class);
$this->setPrivateProperty($this->model, 'validationRules', ['description' => 'required']);
$this->setPrivateProperty($this->model, 'validationMessages', ['description' => 'Description field is required.']);
$this->assertFalse($this->model->insertBatch($jobData));
$error = $this->model->getValidationMessages();
$this->assertSame('Description field is required.', $error['description']);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/6577
*/
public function testUpdateEntityWithPropertyCleanValidationRulesTrueAndCallingCleanRulesFalse()
{
$model = new class () extends Model {
protected $table = 'test';
protected $allowedFields = ['field1', 'field2', 'field3', 'field4'];
protected $returnType = SimpleEntity::class;
protected $validationRules = [
'field1' => 'required_with[field2,field3,field4]',
'field2' => 'permit_empty',
'field3' => 'permit_empty',
'field4' => 'permit_empty',
];
};
// Simulate to get the entity from the database.
$entity = new SimpleEntity();
$entity->setAttributes([
'id' => '1',
'field1' => 'value1',
'field2' => 'value2',
'field3' => '',
'field4' => '',
]);
// Change field1 value.
$entity->field1 = '';
// Set $cleanValidationRules to false by cleanRules()
$model->cleanRules(false)->save($entity);
$errors = $model->errors();
$this->assertCount(1, $errors);
$this->assertSame(
$errors['field1'],
'The field1 field is required when field2,field3,field4 is present.'
);
}
public function testUpdateEntityWithPropertyCleanValidationRulesFalse()
{
$model = new class () extends Model {
protected $table = 'test';
protected $allowedFields = ['field1', 'field2', 'field3', 'field4'];
protected $returnType = SimpleEntity::class;
protected $validationRules = [
'field1' => 'required_with[field2,field3,field4]',
'field2' => 'permit_empty',
'field3' => 'permit_empty',
'field4' => 'permit_empty',
];
// Set to false.
protected $cleanValidationRules = false;
};
// Simulate to get the entity from the database.
$entity = new SimpleEntity();
$entity->setAttributes([
'id' => '1',
'field1' => 'value1',
'field2' => 'value2',
'field3' => '',
'field4' => '',
]);
// Change field1 value.
$entity->field1 = '';
$model->save($entity);
$errors = $model->errors();
$this->assertCount(1, $errors);
$this->assertSame(
$errors['field1'],
'The field1 field is required when field2,field3,field4 is present.'
);
}
public function testInsertEntityValidateEntireRules()
{
$model = new class () extends Model {
protected $table = 'test';
protected $allowedFields = ['field1', 'field2', 'field3', 'field4'];
protected $returnType = SimpleEntity::class;
protected $validationRules = [
'field1' => 'required',
'field2' => 'required',
'field3' => 'permit_empty',
'field4' => 'permit_empty',
];
};
$entity = new SimpleEntity();
$entity->setAttributes([
'field1' => 'value1',
// field2 is missing
'field3' => '',
'field4' => '',
]);
// Insert ignores $cleanValidationRules value.
$model->insert($entity);
$errors = $model->errors();
$this->assertCount(1, $errors);
$this->assertSame(
$errors['field2'],
'The field2 field is required.'
);
}
}

View File

@ -343,7 +343,7 @@ final class FeatureTestTraitTest extends CIUnitTestCase
public function testOpenCliRoutesFromHttpGot404($from, $to, $httpGet)
{
$this->expectException(PageNotFoundException::class);
$this->expectExceptionMessage('Cannot access the controller in a CLI Route.');
$this->expectExceptionMessage('Cannot access CLI Route: ');
$collection = Services::routes();
$collection->setAutoRoute(true);

View File

@ -113,6 +113,17 @@ final class CellTest extends CIUnitTestCase
$this->assertSame($expected, $this->cell->render('\Tests\Support\View\SampleClass::hello'));
}
public function testDisplayRendersTwoCellsWithSameShortName()
{
$output = $this->cell->render('\Tests\Support\View\SampleClass::hello');
$this->assertSame('Hello', $output);
$output = $this->cell->render('\Tests\Support\View\OtherCells\SampleClass::hello');
$this->assertSame('Good-bye!', $output);
}
public function testDisplayRendersWithValidParamString()
{
$params = 'one=two,three=four';

View File

@ -12,9 +12,16 @@ Release Date: Unreleased
BREAKING
********
- **RouteCollection:** The second parameter ``bool $includeWildcard = true`` has
been added to the ``RouteCollection::getRoutes()`` method.
- **AutoRouting Legacy:** The first parameter of the ``AutoRouter::__construct()``
has been changed from ``$protectedControllers`` to ``$cliRoutes``.
- **FeatureTestTrait:** When using :ref:`withBodyFormat() <feature-formatting-the-request>`,
the priority of the request body has been changed.
See :ref:`Upgrading Guide <upgrade-437-feature-testing>` for details.
- **Validation:** The return value of ``Validation::loadRuleGroup()`` has been
changed from "**rules array**" to "**array** of **rules array** and **customErrors array**"
(``[rules, customErrors]``).
Message Changes
***************
@ -22,12 +29,20 @@ Message Changes
Changes
*******
- The number helper function :php:func:`number_to_amount()`, which previously
returned "1000", has been corrected to return "1 thousand" when the number
is exactly 1000, for example.
Deprecations
************
Bugs Fixed
**********
- **AutoRouting Legacy:** Fixed a bug that when you added a route with
``$routes->add()``, the controller's other methods were inaccessible from the
web browser.
See the repo's
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
for a complete list of bugs fixed.

View File

@ -99,10 +99,12 @@ Convenience Functions
Two shortcut functions for Factories have been provided. These functions are always available.
.. _factories-config:
config()
========
The first is ``config()`` which returns a new instance of a Config class. The only required parameter is the class name:
The first is :php:func:`config()` which returns a new instance of a Config class. The only required parameter is the class name:
.. literalinclude:: factories/008.php

View File

@ -30,6 +30,21 @@ Service Accessors
.. literalinclude:: common_functions/001.php
.. php:function:: config(string $name[, bool $getShared = true])
:param string $name: The config classname.
:param bool $getShared: Whether to return a shared instance.
:returns: The config instances.
:rtype: object|null
More simple way of getting config instances from Factories.
See :ref:`Configuration <configuration-config>` and
:ref:`Factories <factories-config>` for details.
The ``config()`` uses ``Factories::config()`` internally.
See :ref:`factories-loading-class` for details on the first parameter ``$name``.
.. php:function:: cookie(string $name[, string $value = ''[, array $options = []]])
:param string $name: Cookie name

View File

@ -30,10 +30,12 @@ By using the ``new`` keyword to create an instance:
.. literalinclude:: configuration/001.php
.. _configuration-config:
config()
--------
By using the ``config()`` function:
By using the :php:func:`config()` function:
.. literalinclude:: configuration/002.php

View File

@ -189,7 +189,7 @@ with the ``new`` command:
.. literalinclude:: modules/008.php
Config files are automatically discovered whenever using the ``config()`` function that is always available.
Config files are automatically discovered whenever using the :php:func:`config()` function that is always available.
.. note:: We don't recommend you use the same short classname in modules.
Modules that need to override or add to known configurations in **app/Config/** should use :ref:`registrars`.

View File

@ -324,10 +324,12 @@ Command-Line Only Routes
.. note:: It is recommended to use Spark Commands for CLI scripts instead of calling controllers via CLI.
See the :doc:`../cli/cli_commands` page for detailed information.
You can create routes that work only from the command-line, and are inaccessible from the web browser, with the
``cli()`` method. Any route created by any of the HTTP-verb-based
Any route created by any of the HTTP-verb-based
route methods will also be inaccessible from the CLI, but routes created by the ``add()`` method will still be
available from the command line:
available from the command line.
You can create routes that work only from the command-line, and are inaccessible from the web browser, with the
``cli()`` method:
.. literalinclude:: routing/032.php

View File

@ -1,9 +1,10 @@
################
Running Your App
################
.. contents::
:local:
:depth: 2
:depth: 3
A CodeIgniter 4 app can be run in a number of different ways: hosted on a web server,
using virtualization, or using CodeIgniter's command line tool for testing.
@ -20,8 +21,9 @@ section of the User Guide to begin learning how to build dynamic PHP application
.. _initial-configuration:
*********************
Initial Configuration
=====================
*********************
#. Open the **app/Config/App.php** file with a text editor and
set your base URL to ``$baseURL``. If you need more flexibility, the baseURL may
@ -49,11 +51,12 @@ Initial Configuration
your project, so that it is writable by the user or account used by your
web server.
************************
Local Development Server
========================
************************
CodeIgniter 4 comes with a local development server, leveraging PHP's built-in web server
with CodeIgniter routing. You can launch it, with the following command line
with CodeIgniter routing. You can launch it, with the following command line
in the main directory::
> php spark serve
@ -83,8 +86,9 @@ The local development server can be customized with three command line options:
> php spark serve --php /usr/bin/php7.6.5.4
*******************
Hosting with Apache
===================
*******************
A CodeIgniter4 webapp is normally hosted on a web server.
Apache HTTP Server is the "standard" platform, and assumed in much of our documentation.
@ -92,19 +96,29 @@ Apache HTTP Server is the "standard" platform, and assumed in much of our docume
Apache is bundled with many platforms, but can also be downloaded in a bundle
with a database engine and PHP from `Bitnami <https://bitnami.com/stacks/infrastructure>`_.
.htaccess
---------
Configure Main Config File
==========================
Enabling mod_rewrite
--------------------
The "mod_rewrite" module enables URLs without "index.php" in them, and is assumed
in our user guide.
Make sure that the rewrite module is enabled (uncommented) in the main
configuration file, e.g., **apache2/conf/httpd.conf**::
configuration file, e.g., **apache2/conf/httpd.conf**:
.. code-block:: apache
LoadModule rewrite_module modules/mod_rewrite.so
Setting Document Root
---------------------
Also make sure that the default document root's ``<Directory>`` element enables this too,
in the ``AllowOverride`` setting::
in the ``AllowOverride`` setting:
.. code-block:: apache
<Directory "/opt/lamp/apache2/htdocs">
Options Indexes FollowSymLinks
@ -112,111 +126,61 @@ in the ``AllowOverride`` setting::
Require all granted
</Directory>
Removing the index.php
----------------------
See :ref:`CodeIgniter URLs <urls-remove-index-php-apache>`.
Virtual Hosting
---------------
Hosting with VirtualHost
========================
We recommend using "virtual hosting" to run your apps.
You can set up different aliases for each of the apps you work on,
Enabling vhost_alias_module
---------------------------
Make sure that the virtual hosting module is enabled (uncommented) in the main
configuration file, e.g., **apache2/conf/httpd.conf**::
configuration file, e.g., **apache2/conf/httpd.conf**:
.. code-block:: apache
LoadModule vhost_alias_module modules/mod_vhost_alias.so
Adding Host Alias
-----------------
Add a host alias in your "hosts" file, typically **/etc/hosts** on unix-type platforms,
or **c:/Windows/System32/drivers/etc/hosts** on Windows.
Add a line to the file. This could be ``myproject.local`` or ``myproject.test``, for instance::
or **c:\Windows\System32\drivers\etc\hosts** on Windows.
Add a line to the file.
This could be ``myproject.local`` or ``myproject.test``, for instance::
127.0.0.1 myproject.local
Add a ``<VirtualHost>`` element for your webapp inside the virtual hosting configuration,
e.g., **apache2/conf/extra/httpd-vhost.conf**::
<VirtualHost *:80>
DocumentRoot "/opt/lamp/apache2/htdocs/myproject/public"
ServerName myproject.local
ErrorLog "logs/myproject-error_log"
CustomLog "logs/myproject-access_log" common
</VirtualHost>
If your project folder is not a subfolder of the Apache document root, then your
``<VirtualHost>`` element may need a nested ``<Directory>`` element to grant the web server access to the files.
With mod_userdir (Shared Hosts)
--------------------------------
A common practice in shared hosting environments is to use the Apache module "mod_userdir" to enable per-user Virtual Hosts automatically. Additional configuration is required to allow CodeIgniter4 to be run from these per-user directories.
The following assumes that the server is already configured for mod_userdir. A guide to enabling this module is available `in the Apache documentation <https://httpd.apache.org/docs/2.4/howto/public_html.html>`_.
Because CodeIgniter4 expects the server to find the framework front controller at **public/index.php** by default, you must specify this location as an alternative to search for the request (even if CodeIgniter4 is installed within the per-user web directory).
The default user web directory **~/public_html** is specified by the ``UserDir`` directive, typically in **apache2/mods-available/userdir.conf** or **apache2/conf/extra/httpd-userdir.conf**::
UserDir public_html
So you will need to configure Apache to look for CodeIgniter's public directory first before trying to serve the default::
UserDir "public_html/public" "public_html"
Be sure to specify options and permissions for the CodeIgniter public directory as well. A **userdir.conf** might look like::
<IfModule mod_userdir.c>
UserDir "public_html/public" "public_html"
UserDir disabled root
<Directory /home/*/public_html>
AllowOverride All
Options MultiViews Indexes FollowSymLinks
<Limit GET POST OPTIONS>
# Apache <= 2.2:
# Order allow,deny
# Allow from all
# Apache >= 2.4:
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS>
# Apache <= 2.2:
# Order deny,allow
# Deny from all
# Apache >= 2.4:
Require all denied
</LimitExcept>
</Directory>
<Directory /home/*/public_html/public>
AllowOverride All
Options MultiViews Indexes FollowSymLinks
<Limit GET POST OPTIONS>
# Apache <= 2.2:
# Order allow,deny
# Allow from all
# Apache >= 2.4:
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS>
# Apache <= 2.2:
# Order deny,allow
# Deny from all
# Apache >= 2.4:
Require all denied
</LimitExcept>
</Directory>
</IfModule>
Setting Environment
Setting VirtualHost
-------------------
See :ref:`Handling Multiple Environments <environment-apache>`.
Add a ``<VirtualHost>`` element for your webapp inside the virtual hosting configuration,
e.g., **apache2/conf/extra/httpd-vhost.conf**:
.. code-block:: apache
<VirtualHost *:80>
DocumentRoot "/opt/lamp/apache2/myproject/public"
ServerName myproject.local
ErrorLog "logs/myproject-error_log"
CustomLog "logs/myproject-access_log" common
<Directory "/opt/lamp/apache2/myproject/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
The above configuration assumes the project folder is located as follows:
.. code-block:: text
apache2/
├── myproject/ (Project Folder)
│ └── public/ (DocumentRoot for myproject.local)
└── htdocs/
Testing
-------
@ -225,14 +189,97 @@ With the above configuration, your webapp would be accessed with the URL **http:
Apache needs to be restarted whenever you change its configuration.
Hosting with mod_userdir (Shared Hosts)
=======================================
A common practice in shared hosting environments is to use the Apache module "mod_userdir" to enable per-user Virtual Hosts automatically. Additional configuration is required to allow CodeIgniter4 to be run from these per-user directories.
The following assumes that the server is already configured for mod_userdir. A guide to enabling this module is available `in the Apache documentation <https://httpd.apache.org/docs/2.4/howto/public_html.html>`_.
Because CodeIgniter4 expects the server to find the framework front controller at **public/index.php** by default, you must specify this location as an alternative to search for the request (even if CodeIgniter4 is installed within the per-user web directory).
The default user web directory **~/public_html** is specified by the ``UserDir`` directive, typically in **apache2/mods-available/userdir.conf** or **apache2/conf/extra/httpd-userdir.conf**:
.. code-block:: apache
UserDir public_html
So you will need to configure Apache to look for CodeIgniter's public directory first before trying to serve the default:
.. code-block:: apache
UserDir "public_html/public" "public_html"
Be sure to specify options and permissions for the CodeIgniter public directory as well. A **userdir.conf** might look like:
.. code-block:: apache
<IfModule mod_userdir.c>
UserDir "public_html/public" "public_html"
UserDir disabled root
<Directory /home/*/public_html>
AllowOverride All
Options MultiViews Indexes FollowSymLinks
<Limit GET POST OPTIONS>
# Apache <= 2.2:
# Order allow,deny
# Allow from all
# Apache >= 2.4:
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS>
# Apache <= 2.2:
# Order deny,allow
# Deny from all
# Apache >= 2.4:
Require all denied
</LimitExcept>
</Directory>
<Directory /home/*/public_html/public>
AllowOverride All
Options MultiViews Indexes FollowSymLinks
<Limit GET POST OPTIONS>
# Apache <= 2.2:
# Order allow,deny
# Allow from all
# Apache >= 2.4:
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS>
# Apache <= 2.2:
# Order deny,allow
# Deny from all
# Apache >= 2.4:
Require all denied
</LimitExcept>
</Directory>
</IfModule>
Removing the index.php
======================
See :ref:`CodeIgniter URLs <urls-remove-index-php-apache>`.
Setting Environment
===================
See :ref:`Handling Multiple Environments <environment-apache>`.
******************
Hosting with Nginx
==================
******************
Nginx is the second most widely used HTTP server for web hosting.
Here you can find an example configuration using PHP 8.1 FPM (unix sockets) under Ubuntu Server.
default.conf
------------
============
This configuration enables URLs without "index.php" in them and using CodeIgniter's "404 - File Not Found" for URLs ending with ".php".
@ -269,12 +316,13 @@ This configuration enables URLs without "index.php" in them and using CodeIgnite
}
Setting Environment
-------------------
===================
See :ref:`Handling Multiple Environments <environment-nginx>`.
*********************
Bootstrapping the App
=====================
*********************
In some scenarios you will want to load the framework without actually running the whole
application. This is particularly useful for unit testing your project, but may also be

View File

@ -39,6 +39,19 @@ is not used::
Previously, the ``$body`` was used for the request body.
Return value of Validation::loadRuleGroup()
===========================================
The return value of ``Validation::loadRuleGroup()`` has been changed from
"**rules array**" to "**array** of **rules array** and **customErrors array**"
(``[rules, customErrors]``).
If you use the method, update the code like the following::
$rules = $this->validation->loadRuleGroup($rules);
[$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
Breaking Enhancements
*********************

View File

@ -2,7 +2,9 @@
namespace Config;
class Validation
// ...
class Validation extends BaseConfig
{
// ...

View File

@ -2,9 +2,13 @@
namespace Config;
class Validation
// ...
class Validation extends BaseConfig
{
public $signup = [
// ...
public array $signup = [
'username' => 'required|max_length[30]',
'password' => 'required|max_length[255]',
'pass_confirm' => 'required|max_length[255]|matches[password]',

View File

@ -2,16 +2,20 @@
namespace Config;
class Validation
// ...
class Validation extends BaseConfig
{
public $signup = [
// ...
public array $signup = [
'username' => 'required|max_length[30]',
'password' => 'required|max_length[255]',
'pass_confirm' => 'required|max_length[255]|matches[password]',
'email' => 'required|max_length[254]|valid_email',
];
public $signup_errors = [
public array $signup_errors = [
'username' => [
'required' => 'You must choose a username.',
],

View File

@ -2,9 +2,13 @@
namespace Config;
class Validation
// ...
class Validation extends BaseConfig
{
public $signup = [
// ...
public array $signup = [
'username' => [
'rules' => 'required|max_length[30]',
'errors' => [
@ -18,5 +22,6 @@ class Validation
],
],
];
// ...
}

View File

@ -2,9 +2,13 @@
namespace Config;
class Validation
// ...
class Validation extends BaseConfig
{
public $templates = [
// ...
public array $templates = [
'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single',
'my_list' => '_errors_list',

View File

@ -2,12 +2,13 @@
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Validation\CreditCardRules;
use CodeIgniter\Validation\FileRules;
use CodeIgniter\Validation\FormatRules;
use CodeIgniter\Validation\Rules;
class Validation
class Validation extends BaseConfig
{
// ...

View File

@ -150,7 +150,7 @@ Ensure that a header or cookie was actually emitted:
.. literalinclude:: overview/009.php
.. note:: the test case with this should be `run as a separate process
in PHPunit <https://phpunit.readthedocs.io/en/9.5/annotations.html#runinseparateprocess>`_.
in PHPunit <https://docs.phpunit.de/en/9.6/annotations.html#runinseparateprocess>`_.
assertHeaderNotEmitted($header, $ignoreCase = false)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -160,7 +160,7 @@ Ensure that a header or cookie was not emitted:
.. literalinclude:: overview/010.php
.. note:: the test case with this should be `run as a separate process
in PHPunit <https://phpunit.readthedocs.io/en/9.5/annotations.html#runinseparateprocess>`_.
in PHPunit <https://docs.phpunit.de/en/9.6/annotations.html#runinseparateprocess>`_.
assertCloseEnough($expected, $actual, $message = '', $tolerance = 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^