diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 6a857588f64f..b2f86c686252 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -29,6 +29,11 @@ 'ThirdParty', 'Validation/Views', ]) + ->notPath([ + '_support/View/Cells/multiplier.php', + '_support/View/Cells/colors.php', + '_support/View/Cells/addition.php', + ]) ->notName('#Foobar.php$#') ->append([ __FILE__, diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a163c0c7a7..a0233fff0e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [v4.3.6](https://github.com/codeigniter4/CodeIgniter4/tree/v4.3.6) (2023-06-18) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.3.5...v4.3.6) + +### Breaking Changes + +* fix: [Validation] DBGroup is ignored when checking the value of a placeholder by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7549 +* fix: [Auto Routing Improved] feature testing may not find controller/method by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7543 + +### Fixed Bugs + +* fix: feature test with validation by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7548 +* fix: [Postgre] Semicolon in the connection parameters break the DSN string by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/7552 +* fix: [QueryBuilder] incorrect SQL without space before "ON DUPLICATE KEY UPDATE" by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7564 +* fix: wrong classname in exception message in Cell by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7569 +* fix: `imagecreatefrompng()` gd-png: libpng warning by @ping-yee in https://github.com/codeigniter4/CodeIgniter4/pull/7570 + +### Refactoring + +* refactor: remove unneeded code in IncomingRequest by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7525 +* refactor: View by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7534 +* refactor: [Entity] fix incorrect return value by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7542 +* refactor: Database::initDriver() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7553 +* refactor: remove Factories::models() by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/7566 +* refactor: Validation::processRules() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7565 +* refactor: [Auto Routing Improved] ensure $httpVerb is lower case by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7575 + ## [v4.3.5](https://github.com/codeigniter4/CodeIgniter4/tree/v4.3.5) (2023-05-21) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.3.4...v4.3.5) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index be503db641ff..d052ee1dcb0e 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -149,14 +149,6 @@ the existing content. * Create **user_guide_src/source/installation/upgrade_{next_version}.rst** and add it to **upgrading.rst** (See **next-upgrading-guide.rst**) -## After Publishing Security Advisory - -* Send a PR to [PHP Security Advisories Database](https://github.com/FriendsOfPHP/security-advisories). - * E.g. https://github.com/FriendsOfPHP/security-advisories/pull/606 - * See https://github.com/FriendsOfPHP/security-advisories#contributing - * Don't forget to run `php -d memory_limit=-1 validator.php`, before - submitting the PR - ## Appendix ### Sphinx Installation diff --git a/composer.json b/composer.json index 08af7d632a0f..08c6d72d8d18 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "0.16.0", + "rector/rector": "0.17.1", "vimeo/psalm": "^5.0" }, "suggest": { diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index a3bc07fd877b..a32d4fc11a79 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -120,11 +120,6 @@ parameters: count: 1 path: system/HTTP/Files/UploadedFile.php - - - message: "#^Property CodeIgniter\\\\HTTP\\\\IncomingRequest\\:\\:\\$locale \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/HTTP/IncomingRequest.php - - message: "#^Property CodeIgniter\\\\HTTP\\\\Message\\:\\:\\$protocolVersion \\(string\\) on left side of \\?\\? is not nullable\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9816237b9c82..0c8230dcebdd 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,44 +1,34 @@ - - - - require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php' - - + - + Memcache Memcache Memcache - - $this->memcached - $this->memcached - $this->memcached - $this->memcached - $this->memcached - $this->memcached + + memcached]]> + memcached]]> + memcached]]> + memcached]]> + memcached]]> + memcached]]> Memcache|Memcached - - - $routeWithoutController - - - + $routeWithoutController $routeWithoutController - - $this->db->transStatus + + db->transStatus]]> - + OCI_COMMIT_ON_SUCCESS OCI_COMMIT_ON_SUCCESS OCI_COMMIT_ON_SUCCESS @@ -46,167 +36,99 @@ SQLT_CHR - - - SQLSRV_ENC_CHAR - SQLSRV_ERR_ERRORS - SQLSRV_ERR_ERRORS - - - - - SQLSRV_FETCH_ASSOC - SQLSRV_SQLTYPE_BIGINT - SQLSRV_SQLTYPE_BIT - SQLSRV_SQLTYPE_CHAR - SQLSRV_SQLTYPE_DATE - SQLSRV_SQLTYPE_DATETIME - SQLSRV_SQLTYPE_DATETIME2 - SQLSRV_SQLTYPE_DATETIMEOFFSET - SQLSRV_SQLTYPE_DECIMAL - SQLSRV_SQLTYPE_FLOAT - SQLSRV_SQLTYPE_IMAGE - SQLSRV_SQLTYPE_INT - SQLSRV_SQLTYPE_MONEY - SQLSRV_SQLTYPE_NCHAR - SQLSRV_SQLTYPE_NTEXT - SQLSRV_SQLTYPE_NUMERIC - SQLSRV_SQLTYPE_NVARCHAR - SQLSRV_SQLTYPE_REAL - SQLSRV_SQLTYPE_SMALLDATETIME - SQLSRV_SQLTYPE_SMALLINT - SQLSRV_SQLTYPE_SMALLMONEY - SQLSRV_SQLTYPE_TEXT - SQLSRV_SQLTYPE_TIME - SQLSRV_SQLTYPE_TIMESTAMP - SQLSRV_SQLTYPE_TINYINT - SQLSRV_SQLTYPE_UDT - SQLSRV_SQLTYPE_UNIQUEIDENTIFIER - SQLSRV_SQLTYPE_VARBINARY - SQLSRV_SQLTYPE_VARCHAR - SQLSRV_SQLTYPE_XML - - - + renderTimeline - + $config - + $timestamp - + $output[$name] - + $count $count $count - + #[ReturnTypeWillChange] #[ReturnTypeWillChange] #[ReturnTypeWillChange] + + + dom = &$this->domParser]]> + + - + $filters - + $routes $routes - - - $value - - - + $this - - - $greeting - $name - - - - - $items - - - - - $value - - - - - $message - - - + $command - - 'JobModel' + UnexsistenceClass - - - SimpleConfig - - - - 'SomeWidget' + + - - $db->username - $db->username + + username]]> + username]]> - + OCI_ASSOC OCI_B_CURSOR OCI_RETURN_NULLS - + $current[$key] - - $_SESSION['_ci_old_input'] + + - + NeverHeardOfIt diff --git a/psalm.xml b/psalm.xml index edd43a2844ff..45242620028f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -8,6 +8,8 @@ autoloader="psalm_autoload.php" cacheDirectory="build/psalm/" errorBaseline="psalm-baseline.xml" + findUnusedBaselineEntry="false" + findUnusedCode="false" > diff --git a/psalm_autoload.php b/psalm_autoload.php index 374964846b1f..d0f0c9af46e6 100644 --- a/psalm_autoload.php +++ b/psalm_autoload.php @@ -23,4 +23,23 @@ } } +$dirs = [ + 'tests/_support/Controllers', +]; + +foreach ($dirs as $dir) { + $dir = __DIR__ . '/' . $dir; + if (! is_dir($dir)) { + continue; + } + + chdir($dir); + + foreach (glob('*.php') as $filename) { + $filePath = realpath($dir . '/' . $filename); + + require_once $filePath; + } +} + chdir(__DIR__); diff --git a/rector.php b/rector.php index d79d47cb3fee..df51a9b43b5a 100644 --- a/rector.php +++ b/rector.php @@ -12,9 +12,7 @@ use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector; use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector; use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector; -use Rector\CodeQuality\Rector\For_\ForToForeachRector; use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector; -use Rector\CodeQuality\Rector\FuncCall\AddPregQuoteDelimiterRector; use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector; use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector; use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector; @@ -28,9 +26,9 @@ use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; -use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector; use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; @@ -44,7 +42,6 @@ use Rector\PHPUnit\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; -use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; use Utils\Rector\PassStrictParameterToFunctionParameterRector; @@ -90,9 +87,9 @@ __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', ], - // call on purpose for nothing happen check - RemoveEmptyMethodCallRector::class => [ - __DIR__ . '/tests', + RemoveUnusedConstructorParamRector::class => [ + // @TODO remove if deprecated $httpVerb is removed + __DIR__ . '/system/Router/AutoRouterImproved.php', ], // check on constant compare @@ -115,6 +112,8 @@ GetMockBuilderGetMockToCreateMockRector::class => [ __DIR__ . '/tests/system/Email/EmailTest.php', ], + + SimplifyRegexPatternRector::class, ]); // auto import fully qualified class names @@ -125,7 +124,6 @@ $rectorConfig->rule(RemoveAlwaysElseRector::class); $rectorConfig->rule(PassStrictParameterToFunctionParameterRector::class); $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); - $rectorConfig->rule(ForToForeachRector::class); $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); $rectorConfig->rule(SimplifyStrposLowerRector::class); @@ -140,12 +138,10 @@ $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); $rectorConfig->rule(RemoveErrorSuppressInTryCatchStmtsRector::class); $rectorConfig->rule(RemoveVarTagFromClassConstantRector::class); - $rectorConfig->rule(AddPregQuoteDelimiterRector::class); $rectorConfig->rule(SimplifyRegexPatternRector::class); $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); - $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); $rectorConfig->rule(StringClassNameToClassConstantRector::class); $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); $rectorConfig->rule(CompleteDynamicPropertiesRector::class); diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index f58d5c791d97..a4f7c67677d0 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -33,8 +33,13 @@ public function __construct(Autoloader $autoloader) * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The namespaced file to locate - * @param string|null $folder The folder within the namespace that we should look for the file. + * @param string $file The relative file path or namespaced file to + * locate. If not namespaced, search in the app + * folder. + * @param string|null $folder The folder within the namespace that we should + * look for the file. If $file does not contain + * this value, it will be appended to the namespace + * folder. * @param string $ext The file extension the file should have. * * @return false|string The path to the file, or false if not found. diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 943fa360d1ce..829b65ea952b 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -47,7 +47,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.3.5'; + public const CI_VERSION = '4.3.6'; /** * App startup time. @@ -310,8 +310,6 @@ private function configureKint(): void * makes all of the pieces work together. * * @return ResponseInterface|void - * - * @throws RedirectException */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) { diff --git a/system/Common.php b/system/Common.php index 2a98253cb6b4..8d366973f14d 100644 --- a/system/Common.php +++ b/system/Common.php @@ -744,11 +744,7 @@ function is_windows(?bool $mock = null): bool $mocked = $mock; } - if (isset($mocked)) { - return $mocked; - } - - return DIRECTORY_SEPARATOR === '\\'; + return $mocked ?? DIRECTORY_SEPARATOR === '\\'; } } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 359fcc1488ba..300e9ffac5f9 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -86,7 +86,7 @@ public function __construct() /** * Initialization an environment-specific configuration setting * - * @param mixed $property + * @param array|bool|float|int|string|null $property * * @return void */ diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 7abd3f5c4449..d752f55c506e 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -24,6 +24,7 @@ * instantiation checks. * * @method static BaseConfig|null config(...$arguments) + * @method static Model|null models(string $name, array $options = [], ?ConnectionInterface &$conn = null) */ class Factories { @@ -69,23 +70,6 @@ class Factories */ protected static $instances = []; - /** - * This method is only to prevent PHPStan error. - * If we have a solution, we can remove this method. - * See https://github.com/codeigniter4/CodeIgniter4/pull/5358 - * - * @template T of Model - * - * @phpstan-param class-string $name - * - * @return Model - * @phpstan-return T - */ - public static function models(string $name, array $options = [], ?ConnectionInterface &$conn = null) - { - return self::__callStatic('models', [$name, $options, $conn]); - } - /** * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index fab6e30d2362..07486cffaeec 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1997,7 +1997,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri } if (isset($this->QBOptions['setQueryAsData'])) { - $data = $this->QBOptions['setQueryAsData']; + $data = $this->QBOptions['setQueryAsData'] . "\n"; } else { $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n"; } diff --git a/system/Database/Config.php b/system/Database/Config.php index eedbbd441465..802a4da25ac6 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -37,7 +37,7 @@ class Config extends BaseConfig protected static $factory; /** - * Creates the default + * Returns the database connection * * @param array|BaseConnection|string|null $group The name of the connection group to use, * or an array of configuration settings. diff --git a/system/Database/Database.php b/system/Database/Database.php index df3ad9ae1550..2818fe220928 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -16,7 +16,7 @@ /** * Database Connection Factory * - * Creates and returns an instance of the appropriate DatabaseConnection + * Creates and returns an instance of the appropriate Database Connection. */ class Database { @@ -32,8 +32,7 @@ class Database protected $connections = []; /** - * Parses the connection binds and returns an instance of the driver - * ready to go. + * Parses the connection binds and creates a Database Connection instance. * * @return BaseConnection * @@ -83,7 +82,7 @@ public function loadUtils(ConnectionInterface $db): BaseUtils } /** - * Parse universal DSN string + * Parses universal DSN string * * @throws InvalidArgumentException */ @@ -121,21 +120,20 @@ protected function parseDSN(array $params): array } /** - * Initialize database driver. + * Creates a database object. * * @param string $driver Driver name. FQCN can be used. - * @param array|object $argument + * @param string $class 'Connection'|'Forge'|'Utils' + * @param array|object $argument The constructor parameter. * * @return BaseConnection|BaseUtils|Forge */ protected function initDriver(string $driver, string $class, $argument): object { - $class = $driver . '\\' . $class; + $classname = (strpos($driver, '\\') === false) + ? "CodeIgniter\\Database\\{$driver}\\{$class}" + : $driver . '\\' . $class; - if (strpos($driver, '\\') === false) { - $class = "CodeIgniter\\Database\\{$class}"; - } - - return new $class($argument); + return new $classname($argument); } } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 8e96128264e2..56905ec922d2 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -64,14 +64,11 @@ public function connect(bool $persistent = false) $this->buildDSN(); } - // Strip pgsql if exists + // Convert DSN string if (mb_strpos($this->DSN, 'pgsql:') === 0) { - $this->DSN = mb_substr($this->DSN, 6); + $this->convertDSN(); } - // Convert semicolons to spaces. - $this->DSN = str_replace(';', ' ', $this->DSN); - $this->connID = $persistent === true ? pg_pconnect($this->DSN) : pg_connect($this->DSN); if ($this->connID !== false) { @@ -92,6 +89,44 @@ public function connect(bool $persistent = false) return $this->connID; } + /** + * Converts the DSN with semicolon syntax. + */ + private function convertDSN() + { + // Strip pgsql + $this->DSN = mb_substr($this->DSN, 6); + + // Convert semicolons to spaces in DSN format like: + // pgsql:host=localhost;port=5432;dbname=database_name + // https://www.php.net/manual/en/function.pg-connect.php + $allowedParams = ['host', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service']; + + $parameters = explode(';', $this->DSN); + + $output = ''; + $previousParameter = ''; + + foreach ($parameters as $parameter) { + [$key, $value] = explode('=', $parameter, 2); + if (in_array($key, $allowedParams, true)) { + if ($previousParameter !== '') { + if (array_search($key, $allowedParams, true) < array_search($previousParameter, $allowedParams, true)) { + $output .= ';'; + } else { + $output .= ' '; + } + } + $output .= $parameter; + $previousParameter = $key; + } else { + $output .= ';' . $parameter; + } + } + + $this->DSN = $output; + } + /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 81b25eeff5f5..fda9906240cb 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -149,7 +149,7 @@ public static function createKey($length = 32) * * @param string $key Property name * - * @return mixed + * @return array|string|null */ public function __get($key) { diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 13f60e970362..64195672439e 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -61,7 +61,7 @@ protected static function substr($str, $start, $length = null) * * @param string $key Property name * - * @return mixed + * @return array|bool|int|string|null */ public function __get($key) { diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 4247c82b66fe..07e1e26393c8 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -429,7 +429,7 @@ public function cast(?bool $cast = null) * * @param array|bool|float|int|object|string|null $value * - * @return $this + * @return void * * @throws Exception */ @@ -452,7 +452,7 @@ public function __set(string $key, $value = null) if (method_exists($this, $method)) { $this->{$method}($value); - return $this; + return; } // Otherwise, just the value. This allows for creation of new @@ -460,8 +460,6 @@ public function __set(string $key, $value = null) // saved. Useful for grabbing values through joins, assigning // relationships, etc. $this->attributes[$dbColumn] = $value; - - return $this; } /** diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index 744f4f906d89..333f999df84a 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -46,7 +46,7 @@ public static function forMissingExtension(string $extension) 'The framework needs the following extension(s) installed and loaded: %s.', $extension ); - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreEnd } else { $message = lang('Core.missingExtension', [$extension]); } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 29529fd5bef0..8eeaefdb0ed8 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -554,7 +554,7 @@ public function setLocale(string $locale) */ public function getLocale(): string { - return $this->locale ?? $this->defaultLocale; + return $this->locale; } /** diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 6063ef94496b..840c1bb2e4eb 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -354,7 +354,7 @@ protected function getImageResource(string $path, int $imageType) throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported')); } - return imagecreatefrompng($path); + return @imagecreatefrompng($path); case IMAGETYPE_WEBP: if (! function_exists('imagecreatefromwebp')) { diff --git a/system/Model.php b/system/Model.php index 2df1bb2f5132..80e4bb48a34c 100644 --- a/system/Model.php +++ b/system/Model.php @@ -800,11 +800,7 @@ public function __get(string $name) return parent::__get($name); } - if (isset($this->builder()->{$name})) { - return $this->builder()->{$name}; - } - - return null; + return $this->builder()->{$name} ?? null; } /** diff --git a/system/Router/AutoRouter.php b/system/Router/AutoRouter.php index 3c29ff8290d4..3096b3bbd6f9 100644 --- a/system/Router/AutoRouter.php +++ b/system/Router/AutoRouter.php @@ -80,7 +80,7 @@ public function __construct( * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array + public function getRoute(string $uri, string $httpVerb): array { $segments = explode('/', $uri); diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 39b264ecf780..6f2aa1af5ece 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -58,11 +58,6 @@ final class AutoRouterImproved implements AutoRouterInterface */ private bool $translateURIDashes; - /** - * HTTP verb for the request. - */ - private string $httpVerb; - /** * The namespace for controllers. */ @@ -74,15 +69,17 @@ final class AutoRouterImproved implements AutoRouterInterface private string $defaultController; /** - * The name of the default method + * The name of the default method without HTTP verb prefix. */ private string $defaultMethod; /** * @param class-string[] $protectedControllers * @param string $defaultController Short classname + * + * @deprecated $httpVerb is deprecated. No longer used. */ - public function __construct( + public function __construct(// @phpstan-ignore-line array $protectedControllers, string $namespace, string $defaultController, @@ -93,13 +90,11 @@ public function __construct( $this->protectedControllers = $protectedControllers; $this->namespace = rtrim($namespace, '\\') . '\\'; $this->translateURIDashes = $translateURIDashes; - $this->httpVerb = $httpVerb; $this->defaultController = $defaultController; - $this->defaultMethod = $httpVerb . ucfirst($defaultMethod); + $this->defaultMethod = $defaultMethod; // Set the default values $this->controller = $this->defaultController; - $this->method = $this->defaultMethod; } /** @@ -107,8 +102,13 @@ public function __construct( * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array + public function getRoute(string $uri, string $httpVerb): array { + $httpVerb = strtolower($httpVerb); + + $defaultMethod = $httpVerb . ucfirst($this->defaultMethod); + $this->method = $defaultMethod; + $segments = explode('/', $uri); // WARNING: Directories get shifted out of the segments array. @@ -144,10 +144,10 @@ public function getRoute(string $uri): array $methodSegment = $this->translateURIDashes(array_shift($nonDirSegments)); // Prefix HTTP verb - $this->method = $this->httpVerb . ucfirst($methodSegment); + $this->method = $httpVerb . ucfirst($methodSegment); // Prevent access to default method path - if (strtolower($this->method) === strtolower($this->defaultMethod)) { + if (strtolower($this->method) === strtolower($defaultMethod)) { throw new PageNotFoundException( 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); diff --git a/system/Router/AutoRouterInterface.php b/system/Router/AutoRouterInterface.php index 9ecdd3ec2b30..6d98aec4a5bf 100644 --- a/system/Router/AutoRouterInterface.php +++ b/system/Router/AutoRouterInterface.php @@ -21,5 +21,5 @@ interface AutoRouterInterface * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array; + public function getRoute(string $uri, string $httpVerb): array; } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 50b73616ea0e..66724935e31c 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -150,7 +150,7 @@ class RouteCollection implements RouteCollectionInterface /** * The current method that the script is being called by. * - * @var string + * @var string HTTP verb (lower case) like `get`,`post` or `*` */ protected $HTTPVerb = '*'; @@ -550,11 +550,13 @@ public function getHTTPVerb(): string * Sets the current HTTP verb. * Used primarily for testing. * + * @param string $verb HTTP verb + * * @return $this */ public function setHTTPVerb(string $verb) { - $this->HTTPVerb = $verb; + $this->HTTPVerb = strtolower($verb); return $this; } diff --git a/system/Router/Router.php b/system/Router/Router.php index 8dd35c656e39..f9299fa1dd0a 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -126,7 +126,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request $this->controller = $this->collection->getDefaultController(); $this->method = $this->collection->getDefaultMethod(); - $this->collection->setHTTPVerb(strtolower($request->getMethod() ?? $_SERVER['REQUEST_METHOD'])); + $this->collection->setHTTPVerb($request->getMethod() ?? $_SERVER['REQUEST_METHOD']); $this->translateURIDashes = $this->collection->shouldTranslateURIDashes(); @@ -504,7 +504,7 @@ protected function checkRoutes(string $uri): bool public function autoRoute(string $uri) { [$this->directory, $this->controller, $this->method, $this->params] - = $this->autoRouter->getRoute($uri); + = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb()); } /** diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php index 2c2233df81b0..bf157aea52af 100644 --- a/system/Test/FeatureTestCase.php +++ b/system/Test/FeatureTestCase.php @@ -148,9 +148,6 @@ public function skipEvents() * instance that can be used to run many assertions against. * * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException */ public function call(string $method, string $path, ?array $params = null) { diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index e6e8492ea466..f7a6d0609b19 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -138,9 +138,6 @@ public function skipEvents() * instance that can be used to run many assertions against. * * @return TestResponse - * - * @throws RedirectException - * @throws Exception */ public function call(string $method, string $path, ?array $params = null) { @@ -175,6 +172,9 @@ public function call(string $method, string $path, ?array $params = null) // Make sure filters are reset between tests Services::injectMock('filters', Services::filters(null, false)); + // Make sure validation is reset between tests + Services::injectMock('validation', Services::validation(null, false)); + $response = $this->app ->setContext('web') ->setRequest($request) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 93b86ea6b1dd..1bb2c243ebf5 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -263,7 +263,6 @@ public function required_with($str = null, ?string $fields = null, array $data = // 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. - $fields = explode(',', $fields); $present = $this->required($str ?? ''); if ($present) { @@ -272,11 +271,14 @@ public function required_with($str = null, ?string $fields = null, array $data = // Still here? Then we fail this test if // any of the fields are present in $data - // as $fields is the lis + // as $fields is the list $requiredFields = []; - foreach ($fields as $field) { - if ((array_key_exists($field, $data) && ! empty($data[$field])) || (strpos($field, '.') !== false && ! empty(dot_array_search($field, $data)))) { + foreach (explode(',', $fields) as $field) { + if ( + (array_key_exists($field, $data) && ! empty($data[$field])) + || (strpos($field, '.') !== false && ! empty(dot_array_search($field, $data))) + ) { $requiredFields[] = $field; } } @@ -285,7 +287,7 @@ public function required_with($str = null, ?string $fields = null, array $data = } /** - * The field is required when all of the other fields are present + * The field is required when all the other fields are present * in the data but not required. * * Example (field is required when the id or email field is missing): @@ -296,8 +298,13 @@ public function required_with($str = null, ?string $fields = null, array $data = * @param string|null $otherFields The param fields of required_without[]. * @param string|null $field This rule param fields aren't present, this field is required. */ - public function required_without($str = null, ?string $otherFields = null, array $data = [], ?string $error = null, ?string $field = null): bool - { + public function required_without( + $str = null, + ?string $otherFields = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { if ($otherFields === null || empty($data)) { throw new InvalidArgumentException('You must supply the parameters: otherFields, data.'); } @@ -305,8 +312,7 @@ public function required_without($str = null, ?string $otherFields = null, array // 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. - $otherFields = explode(',', $otherFields); - $present = $this->required($str ?? ''); + $present = $this->required($str ?? ''); if ($present) { return true; @@ -314,10 +320,14 @@ public function required_without($str = null, ?string $otherFields = null, array // Still here? Then we fail this test if // any of the fields are not present in $data - foreach ($otherFields as $otherField) { - if ((strpos($otherField, '.') === false) && (! array_key_exists($otherField, $data) || empty($data[$otherField]))) { + foreach (explode(',', $otherFields) as $otherField) { + if ( + (strpos($otherField, '.') === false) + && (! array_key_exists($otherField, $data) || empty($data[$otherField])) + ) { return false; } + if (strpos($otherField, '.') !== false) { if ($field === null) { throw new InvalidArgumentException('You must supply the parameters: field.'); diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index f95bbe7d4083..cdc20f5e2e4e 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -347,7 +347,7 @@ public function required_with($str = null, ?string $fields = null, array $data = } /** - * The field is required when all of the other fields are present + * The field is required when all the other fields are present * in the data but not required. * * Example (field is required when the id or email field is missing): @@ -358,8 +358,13 @@ public function required_with($str = null, ?string $fields = null, array $data = * @param string|null $otherFields The param fields of required_without[]. * @param string|null $field This rule param fields aren't present, this field is required. */ - public function required_without($str = null, ?string $otherFields = null, array $data = [], ?string $error = null, ?string $field = null): bool - { + public function required_without( + $str = null, + ?string $otherFields = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { return $this->nonStrictRules->required_without($str, $otherFields, $data, $error, $field); } } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 9b77b3c8b075..5e539561271a 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -116,12 +116,15 @@ public function __construct($config, RendererInterface $view) * @param array|null $data The array of data to validate. * @param string|null $group The predefined group of rules to apply. * @param string|null $dbGroup The database group to use. + * + * @TODO Type ?string for $dbGroup should be removed. + * See https://github.com/codeigniter4/CodeIgniter4/issues/6723 */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { $data ??= $this->data; - // i.e. is_unique + // `DBGroup` is a reserved name. For is_unique and is_not_unique $data['DBGroup'] = $dbGroup; $this->loadRuleSets(); @@ -184,17 +187,28 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } /** - * Runs the validation process, returning true or false - * determining whether validation was successful or not. + * Runs the validation process, returning true or false determining whether + * validation was successful or not. * * @param array|bool|float|int|object|string|null $value + * @param array|string $rules * @param string[] $errors + * @param string|null $dbGroup The database group to use. */ - public function check($value, string $rule, array $errors = []): bool + public function check($value, $rules, array $errors = [], $dbGroup = null): bool { $this->reset(); - return $this->setRule('check', null, $rule, $errors)->run(['check' => $value]); + return $this->setRule( + 'check', + null, + $rules, + $errors + )->run( + ['check' => $value], + null, + $dbGroup + ); } /** @@ -204,87 +218,30 @@ public function check($value, string $rule, array $errors = []): bool * so that we can collect all of the first errors. * * @param array|string $value - * @param array|null $rules - * @param array|null $data The array of data to validate, with `DBGroup`. + * @param array $rules + * @param array $data The array of data to validate, with `DBGroup`. * @param string|null $originalField The original asterisk field name like "foo.*.bar". */ protected function processRules( string $field, ?string $label, $value, - $rules = null, - ?array $data = null, + $rules = null, // @TODO remove `= null` + ?array $data = null, // @TODO remove `= null` ?string $originalField = null ): bool { if ($data === null) { throw new InvalidArgumentException('You must supply the parameter: data.'); } - if (in_array('if_exist', $rules, true)) { - $flattenedData = array_flatten_with_dots($data); - $ifExistField = $field; - - if (strpos($field, '.*') !== false) { - // We'll change the dot notation into a PCRE pattern that can be used later - $ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/')); - $dataIsExisting = false; - $pattern = sprintf('/%s/u', $ifExistField); - - foreach (array_keys($flattenedData) as $item) { - if (preg_match($pattern, $item) === 1) { - $dataIsExisting = true; - break; - } - } - } else { - $dataIsExisting = array_key_exists($ifExistField, $flattenedData); - } - - unset($ifExistField, $flattenedData); - - if (! $dataIsExisting) { - // we return early if `if_exist` is not satisfied. we have nothing to do here. - return true; - } - - // Otherwise remove the if_exist rule and continue the process - $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist'); + $rules = $this->processIfExist($field, $rules, $data); + if ($rules === true) { + return true; } - if (in_array('permit_empty', $rules, true)) { - if ( - ! in_array('required', $rules, true) - && (is_array($value) ? $value === [] : trim((string) $value) === '') - ) { - $passed = true; - - foreach ($rules as $rule) { - if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { - $rule = $match[1]; - $param = $match[2]; - - if (! in_array($rule, ['required_with', 'required_without'], true)) { - continue; - } - - // Check in our rulesets - foreach ($this->ruleSetInstances as $set) { - if (! method_exists($set, $rule)) { - continue; - } - - $passed = $passed && $set->{$rule}($value, $param, $data); - break; - } - } - } - - if ($passed === true) { - return true; - } - } - - $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty'); + $rules = $this->processPermitEmpty($value, $rules, $data); + if ($rules === true) { + return true; } foreach ($rules as $i => $rule) { @@ -360,6 +317,92 @@ protected function processRules( return true; } + /** + * @param array $data The array of data to validate, with `DBGroup`. + * + * @return array|true The modified rules or true if we return early + */ + private function processIfExist(string $field, array $rules, array $data) + { + if (in_array('if_exist', $rules, true)) { + $flattenedData = array_flatten_with_dots($data); + $ifExistField = $field; + + if (strpos($field, '.*') !== false) { + // We'll change the dot notation into a PCRE pattern that can be used later + $ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/')); + $dataIsExisting = false; + $pattern = sprintf('/%s/u', $ifExistField); + + foreach (array_keys($flattenedData) as $item) { + if (preg_match($pattern, $item) === 1) { + $dataIsExisting = true; + break; + } + } + } else { + $dataIsExisting = array_key_exists($ifExistField, $flattenedData); + } + + if (! $dataIsExisting) { + // we return early if `if_exist` is not satisfied. we have nothing to do here. + return true; + } + + // Otherwise remove the if_exist rule and continue the process + $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist'); + } + + return $rules; + } + + /** + * @param array|string $value + * @param array $data The array of data to validate, with `DBGroup`. + * + * @return array|true The modified rules or true if we return early + */ + private function processPermitEmpty($value, array $rules, array $data) + { + if (in_array('permit_empty', $rules, true)) { + if ( + ! in_array('required', $rules, true) + && (is_array($value) ? $value === [] : trim((string) $value) === '') + ) { + $passed = true; + + foreach ($rules as $rule) { + if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { + $rule = $match[1]; + $param = $match[2]; + + if (! in_array($rule, ['required_with', 'required_without'], true)) { + continue; + } + + // Check in our rulesets + foreach ($this->ruleSetInstances as $set) { + if (! method_exists($set, $rule)) { + continue; + } + + $passed = $passed && $set->{$rule}($value, $param, $data); + break; + } + } + } + + if ($passed === true) { + return true; + } + } + + $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty'); + } + + return $rules; + } + /** * @param Closure|string $rule */ @@ -679,6 +722,7 @@ protected function fillPlaceholders(array $rules, array $data): array foreach ($placeholderFields as $field) { $validator ??= Services::validation(null, false); + assert($validator instanceof Validation); $placeholderRules = $rules[$field]['rules'] ?? null; @@ -699,7 +743,8 @@ protected function fillPlaceholders(array $rules, array $data): array } // Validate the placeholder field - if (! $validator->check($data[$field], implode('|', $placeholderRules))) { + $dbGroup = $data['DBGroup'] ?? null; + if (! $validator->check($data[$field], $placeholderRules, [], $dbGroup)) { // if fails, do nothing continue; } diff --git a/system/Validation/ValidationInterface.php b/system/Validation/ValidationInterface.php index 8c018b9a0dc2..9336c26e7f62 100644 --- a/system/Validation/ValidationInterface.php +++ b/system/Validation/ValidationInterface.php @@ -32,12 +32,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup * Check; runs the validation process, returning true or false * determining whether or not validation was successful. * - * @param array|bool|float|int|object|string|null $value Value to validate. + * @param array|bool|float|int|object|string|null $value Value to validate. + * @param array|string $rules * @param string[] $errors + * @param string|null $dbGroup The database group to use. * * @return bool True if valid, else false. */ - public function check($value, string $rule, array $errors = []): bool; + public function check($value, $rules, array $errors = [], $dbGroup = null): bool; /** * Takes a Request object and grabs the input data to use from its diff --git a/system/View/Cell.php b/system/View/Cell.php index 2e0fc22b8bb3..b898adc0e754 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -175,9 +175,9 @@ protected function determineClass(string $library): array } // locate and return an instance of the cell - $class = Factories::cells($class); + $object = Factories::cells($class); - if (! is_object($class)) { + if (! is_object($object)) { throw ViewException::forInvalidCellClass($class); } @@ -186,7 +186,7 @@ protected function determineClass(string $library): array } return [ - $class, + $object, $method, ]; } diff --git a/system/View/View.php b/system/View/View.php index 9a39104728df..164d67642305 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -171,20 +171,27 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n // multiple views are called in a view, it won't // clean it unless we mean it to. $saveData ??= $this->saveData; - $fileExt = pathinfo($view, PATHINFO_EXTENSION); - $realPath = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3) - $this->renderVars['view'] = $realPath; + + $fileExt = pathinfo($view, PATHINFO_EXTENSION); + // allow Views as .html, .tpl, etc (from CI3) + $this->renderVars['view'] = empty($fileExt) ? $view . '.php' : $view; + $this->renderVars['options'] = $options ?? []; // Was it cached? if (isset($this->renderVars['options']['cache'])) { - $cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']); + $cacheName = $this->renderVars['options']['cache_name'] + ?? str_replace('.php', '', $this->renderVars['view']); $cacheName = str_replace(['\\', '/'], '', $cacheName); $this->renderVars['cacheName'] = $cacheName; if ($output = cache($this->renderVars['cacheName'])) { - $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']); + $this->logPerformance( + $this->renderVars['start'], + microtime(true), + $this->renderVars['view'] + ); return $output; } @@ -193,7 +200,11 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; if (! is_file($this->renderVars['file'])) { - $this->renderVars['file'] = $this->loader->locateFile($this->renderVars['view'], 'Views', empty($fileExt) ? 'php' : $fileExt); + $this->renderVars['file'] = $this->loader->locateFile( + $this->renderVars['view'], + 'Views', + empty($fileExt) ? 'php' : $fileExt + ); } // locateFile will return an empty string if the file cannot be found. @@ -233,10 +244,16 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $output = $this->decorateOutput($output); - $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']); + $this->logPerformance( + $this->renderVars['start'], + microtime(true), + $this->renderVars['view'] + ); - if (($this->debug && (! isset($options['debug']) || $options['debug'] === true)) - && in_array(DebugToolbar::class, service('filters')->getFiltersClass()['after'], true) + $afterFilters = service('filters')->getFiltersClass()['after']; + if ( + ($this->debug && (! isset($options['debug']) || $options['debug'] === true)) + && in_array(DebugToolbar::class, $afterFilters, true) ) { $toolbarCollectors = config(Toolbar::class)->collectors; @@ -253,7 +270,11 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n // Should we cache? if (isset($this->renderVars['options']['cache'])) { - cache()->save($this->renderVars['cacheName'], $output, (int) $this->renderVars['options']['cache']); + cache()->save( + $this->renderVars['cacheName'], + $output, + (int) $this->renderVars['options']['cache'] + ); } $this->tempData = null; diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index ffdd9cba9bf7..a9c7c3097630 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -33,7 +33,7 @@ class Registrar 'DBDriver' => 'MySQLi', 'DBPrefix' => 'db_', 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), + 'DBDebug' => true, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -52,7 +52,7 @@ class Registrar 'DBDriver' => 'Postgre', 'DBPrefix' => 'db_', 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), + 'DBDebug' => true, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -71,7 +71,7 @@ class Registrar 'DBDriver' => 'SQLite3', 'DBPrefix' => 'db_', 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), + 'DBDebug' => true, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -91,7 +91,7 @@ class Registrar 'DBDriver' => 'SQLSRV', 'DBPrefix' => 'db_', 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), + 'DBDebug' => true, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -110,8 +110,8 @@ class Registrar 'DBDriver' => 'OCI8', 'DBPrefix' => 'db_', 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), - 'charset' => 'utf8', + 'DBDebug' => true, + 'charset' => 'AL32UTF8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', 'encrypt' => false, diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index cf966cb0f5da..fc5f56a66d3b 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -51,64 +51,67 @@ protected function setUp(): void $this->locator = new FileLocator($autoloader); } - public function testLocateFileWorksWithLegacyStructure() + public function testLocateFileNotNamespacedFindsInAppDirectory() { - $file = 'Controllers/Home'; + $file = 'Controllers/Home'; // not namespaced $expected = APPPATH . 'Controllers/Home.php'; $this->assertSame($expected, $this->locator->locateFile($file)); } - public function testLocateFileWithLegacyStructureNotFound() + public function testLocateFileNotNamespacedNotFound() { - $file = 'Unknown'; + $file = 'Unknown'; // not namespaced $this->assertFalse($this->locator->locateFile($file)); } - public function testLocateFileWorksInApplicationDirectory() + public function testLocateFileNotNamespacedFindsWithFolderInAppDirectory() { - $file = 'welcome_message'; + $file = 'welcome_message'; // not namespaced $expected = APPPATH . 'Views/welcome_message.php'; $this->assertSame($expected, $this->locator->locateFile($file, 'Views')); } - public function testLocateFileWorksInApplicationDirectoryWithoutFolder() + public function testLocateFileNotNamespacedFindesWithoutFolderInAppDirectory() { - $file = 'Common'; + $file = 'Common'; // not namespaced $expected = APPPATH . 'Common.php'; $this->assertSame($expected, $this->locator->locateFile($file)); } - public function testLocateFileWorksInNestedApplicationDirectory() + public function testLocateFileNotNamespacedWorksInNestedAppDirectory() { - $file = 'Controllers/Home'; + $file = 'Controllers/Home'; // not namespaced $expected = APPPATH . 'Controllers/Home.php'; + // This works because $file contains `Controllers`. $this->assertSame($expected, $this->locator->locateFile($file, 'Controllers')); } - public function testLocateFileReplacesFolderName() + public function testLocateFileWithFolderNameInFile() { $file = '\App\Views/errors/html/error_404.php'; $expected = APPPATH . 'Views/errors/html/error_404.php'; + // This works because $file contains `Views`. $this->assertSame($expected, $this->locator->locateFile($file, 'Views')); } - public function testLocateFileReplacesFolderNameLegacy() + public function testLocateFileNotNamespacedWithFolderNameInFile() { - $file = 'Views/welcome_message.php'; + $file = 'Views/welcome_message.php'; // not namespaced $expected = APPPATH . 'Views/welcome_message.php'; + // This works because $file contains `Views`. $this->assertSame($expected, $this->locator->locateFile($file, 'Views')); } @@ -118,6 +121,7 @@ public function testLocateFileCanFindNamespacedView() $expected = APPPATH . 'Views/errors/html/error_404.php'; + // The namespace `Errors` (APPPATH . 'Views/errors') + the folder (`html`) + `error_404` $this->assertSame($expected, $this->locator->locateFile($file, 'html')); } diff --git a/tests/system/Database/ConfigTest.php b/tests/system/Database/ConfigTest.php index 4c0cc78ce9a4..3e963749dd57 100644 --- a/tests/system/Database/ConfigTest.php +++ b/tests/system/Database/ConfigTest.php @@ -13,6 +13,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; +use Generator; /** * @internal @@ -190,4 +191,47 @@ public function testConnectionGroupWithDSNPostgreNative() $this->assertTrue($this->getPrivateProperty($conn, 'strictOn')); $this->assertSame([], $this->getPrivateProperty($conn, 'failover')); } + + /** + * @dataProvider convertDSNProvider + * + * @see https://github.com/codeigniter4/CodeIgniter4/issues/7550 + */ + public function testConvertDSN(string $input, string $expected) + { + $this->dsnGroupPostgreNative['DSN'] = $input; + $conn = Config::connect($this->dsnGroupPostgreNative, false); + $this->assertInstanceOf(BaseConnection::class, $conn); + + $method = $this->getPrivateMethodInvoker($conn, 'convertDSN'); + $method(); + + $this->assertSame($expected, $this->getPrivateProperty($conn, 'DSN')); + } + + public function convertDSNProvider(): Generator + { + yield from [ + [ + 'pgsql:host=localhost;port=5432;dbname=database_name;user=username;password=password', + 'host=localhost port=5432 dbname=database_name user=username password=password', + ], + [ + 'pgsql:host=localhost;port=5432;dbname=database_name;user=username;password=we;port=we', + 'host=localhost port=5432 dbname=database_name user=username password=we;port=we', + ], + [ + 'pgsql:host=localhost;port=5432;dbname=database_name', + 'host=localhost port=5432 dbname=database_name', + ], + [ + "pgsql:host=localhost;port=5432;dbname=database_name;options='--client_encoding=UTF8'", + "host=localhost port=5432 dbname=database_name options='--client_encoding=UTF8'", + ], + [ + 'pgsql:host=localhost;port=5432;dbname=database_name;something=stupid', + 'host=localhost port=5432 dbname=database_name;something=stupid', + ], + ]; + } } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 54704a79e4c1..90b42aec7de8 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1513,14 +1513,14 @@ public function testDropKey() public function testAddTextColumnWithConstraint() { // some DBMS do not allow a constraint for type TEXT - $result = $this->forge->addColumn('user', [ + $this->forge->addColumn('user', [ 'text_with_constraint' => ['type' => 'text', 'constraint' => 255, 'default' => ''], ]); $this->assertTrue($this->db->fieldExists('text_with_constraint', 'user')); // SQLSRV requires dropping default constraint before dropping column - $result = $this->forge->dropColumn('user', 'text_with_constraint'); + $this->forge->dropColumn('user', 'text_with_constraint'); $this->db->resetDataCache(); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 65fc0e62d03c..5f95b307d8cf 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -293,7 +293,7 @@ public function testWithHeadersWithEmptyHeaders() } $response = new RedirectResponse(new App()); - $response = $response->withHeaders(); + $response->withHeaders(); $this->assertEmpty($baseResponse->headers()); } diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 4148a0f4a500..8006ad6cf133 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -179,7 +179,7 @@ public function testLanguageFileLoadingReturns() $this->assertNotContains('More', $this->lang->loaded()); $this->assertCount(3, $result); - $result = $this->lang->loadem('More', 'en'); + $this->lang->loadem('More', 'en'); $this->assertContains('More', $this->lang->loaded()); $this->assertCount(1, $this->lang->loaded()); } diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index ec94676be121..5b2fdff532c1 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -57,7 +57,7 @@ public function testAutoRouteFindsDefaultControllerAndMethodGet() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('/'); + = $router->getRoute('/', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -72,7 +72,7 @@ public function testAutoRouteFindsDefaultControllerAndMethodPost() $router = $this->createNewAutoRouter('post'); [$directory, $controller, $method, $params] - = $router->getRoute('/'); + = $router->getRoute('/', 'post'); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -85,7 +85,7 @@ public function testAutoRouteFindsControllerWithFileAndMethod() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod'); + = $router->getRoute('mycontroller/somemethod', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -98,7 +98,7 @@ public function testFindsControllerAndMethodAndParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a'); + = $router->getRoute('mycontroller/somemethod/a', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -115,7 +115,7 @@ public function testUriParamCountIsGreaterThanMethodParams() $router = $this->createNewAutoRouter(); - $router->getRoute('mycontroller/somemethod/a/b'); + $router->getRoute('mycontroller/somemethod/a/b', 'get'); } public function testAutoRouteFindsControllerWithFile() @@ -123,7 +123,7 @@ public function testAutoRouteFindsControllerWithFile() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller'); + = $router->getRoute('mycontroller', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -136,7 +136,7 @@ public function testAutoRouteFindsControllerWithSubfolder() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/mycontroller/somemethod'); + = $router->getRoute('subfolder/mycontroller/somemethod', 'get'); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Mycontroller::class, $controller); @@ -149,7 +149,7 @@ public function testAutoRouteFindsDashedSubfolder() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/mycontroller/somemethod'); + = $router->getRoute('dash-folder/mycontroller/somemethod', 'get'); $this->assertSame('Dash_folder/', $directory); $this->assertSame( @@ -165,7 +165,7 @@ public function testAutoRouteFindsDashedController() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/dash-controller/somemethod'); + = $router->getRoute('dash-folder/dash-controller/somemethod', 'get'); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Dash_controller::class, $controller); @@ -178,7 +178,7 @@ public function testAutoRouteFindsDashedMethod() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/dash-controller/dash-method'); + = $router->getRoute('dash-folder/dash-controller/dash-method', 'get'); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Dash_controller::class, $controller); @@ -191,7 +191,7 @@ public function testAutoRouteFindsDefaultDashFolder() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder'); + = $router->getRoute('dash-folder', 'get'); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Home::class, $controller); @@ -205,7 +205,7 @@ public function testAutoRouteRejectsSingleDot() $router = $this->createNewAutoRouter(); - $router->getRoute('.'); + $router->getRoute('.', 'get'); } public function testAutoRouteRejectsDoubleDot() @@ -214,7 +214,7 @@ public function testAutoRouteRejectsDoubleDot() $router = $this->createNewAutoRouter(); - $router->getRoute('..'); + $router->getRoute('..', 'get'); } public function testAutoRouteRejectsMidDot() @@ -223,7 +223,7 @@ public function testAutoRouteRejectsMidDot() $router = $this->createNewAutoRouter(); - $router->getRoute('foo.bar'); + $router->getRoute('foo.bar', 'get'); } public function testRejectsDefaultControllerPath() @@ -232,7 +232,7 @@ public function testRejectsDefaultControllerPath() $router = $this->createNewAutoRouter(); - $router->getRoute('home'); + $router->getRoute('home', 'get'); } public function testRejectsDefaultControllerAndDefaultMethodPath() @@ -241,7 +241,7 @@ public function testRejectsDefaultControllerAndDefaultMethodPath() $router = $this->createNewAutoRouter(); - $router->getRoute('home/index'); + $router->getRoute('home/index', 'get'); } public function testRejectsDefaultMethodPath() @@ -250,7 +250,7 @@ public function testRejectsDefaultMethodPath() $router = $this->createNewAutoRouter(); - $router->getRoute('mycontroller/index'); + $router->getRoute('mycontroller/index', 'get'); } public function testRejectsControllerWithRemapMethod() @@ -262,6 +262,6 @@ public function testRejectsControllerWithRemapMethod() $router = $this->createNewAutoRouter(); - $router->getRoute('remap/test'); + $router->getRoute('remap/test', 'get'); } } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 6458449a02c9..40c5da39b091 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Router; use CodeIgniter\Config\Services; +use CodeIgniter\controller; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; @@ -237,7 +238,7 @@ public function testAddRecognizesCustomNamespaces() $routes->add('home', 'controller'); $expects = [ - 'home' => '\CodeIgniter\controller', + 'home' => '\\' . controller::class, ]; $routes = $routes->getRoutes(); diff --git a/tests/system/Test/FeatureTestAutoRoutingImprovedTest.php b/tests/system/Test/FeatureTestAutoRoutingImprovedTest.php new file mode 100644 index 000000000000..897697146fb5 --- /dev/null +++ b/tests/system/Test/FeatureTestAutoRoutingImprovedTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Test; + +use CodeIgniter\Events\Events; +use Config\Feature; +use Config\Services; + +/** + * @group Others + * + * @internal + */ +final class FeatureTestAutoRoutingImprovedTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + Events::simulate(true); + + self::initializeRouter(); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + Events::simulate(false); + + Services::reset(); + } + + private static function initializeRouter(): void + { + $routes = Services::routes(); + $routes->resetRoutes(); + $routes->loadRoutes(); + + $routes->setAutoRoute(true); + config(Feature::class)->autoRoutesImproved = true; + + $namespace = 'Tests\Support\Controllers'; + $routes->setDefaultNamespace($namespace); + + $router = Services::router($routes); + + Services::injectMock('router', $router); + } + + public function testCallGet() + { + $response = $this->get('newautorouting'); + + $response->assertSee('Hello'); + } + + public function testCallPost() + { + $response = $this->post('newautorouting/save/1/a/b'); + + $response->assertSee('Saved'); + } +} diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 4636d58d3927..d622a00aaedf 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -102,6 +102,36 @@ public function testCallPostWithBody() $response->assertSee('Hello Mars!'); } + public function testCallValidationTwice() + { + $this->withRoutes([ + [ + 'post', + 'section/create', + static function () { + $validation = Services::validation(); + $validation->setRule('title', 'title', 'required|min_length[3]'); + + $post = Services::request()->getPost(); + + if ($validation->run($post)) { + return 'Okay'; + } + + return 'Invalid'; + }, + ], + ]); + + $response = $this->post('section/create', ['foo' => 'Mars']); + + $response->assertSee('Invalid'); + + $response = $this->post('section/create', ['title' => 'Section Title']); + + $response->assertSee('Okay'); + } + public function testCallPut() { $this->withRoutes([ diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 0ed3ac97e49c..7760401655d6 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -614,6 +614,68 @@ public function requiredWithProvider(): Generator ]; } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/7557 + * + * @dataProvider RequiredWithAndOtherRulesProvider + */ + public function testRequiredWithAndOtherRules(bool $expected, array $data): void + { + $this->validation->setRules([ + 'mustBeADate' => 'required_with[otherField]|permit_empty|valid_date', + ]); + + $result = $this->validation->run($data); + + $this->assertSame($expected, $result); + } + + public function RequiredWithAndOtherRulesProvider(): Generator + { + yield from [ + // `otherField` and `mustBeADate` do not exist + [true, []], + // `mustBeADate` does not exist + [false, ['otherField' => 'exists']], + // ``otherField` does not exist + [true, ['mustBeADate' => '2023-06-12']], + [true, ['mustBeADate' => '']], + [true, ['mustBeADate' => null]], + [true, ['mustBeADate' => []]], + // `otherField` and `mustBeADate` exist + [true, ['mustBeADate' => '', 'otherField' => '']], + [true, ['mustBeADate' => '2023-06-12', 'otherField' => 'exists']], + [true, ['mustBeADate' => '2023-06-12', 'otherField' => '']], + [false, ['mustBeADate' => '', 'otherField' => 'exists']], + [false, ['mustBeADate' => [], 'otherField' => 'exists']], + [false, ['mustBeADate' => null, 'otherField' => 'exists']], + ]; + } + + /** + * @dataProvider RequiredWithAndOtherRuleWithValueZeroProvider + */ + public function testRequiredWithAndOtherRuleWithValueZero(bool $expected, array $data): void + { + $this->validation->setRules([ + 'married' => ['rules' => ['in_list[0,1]']], + 'partner_name' => ['rules' => ['permit_empty', 'required_with[married]', 'alpha_space']], + ]); + + $result = $this->validation->run($data); + + $this->assertSame($expected, $result); + } + + public function RequiredWithAndOtherRuleWithValueZeroProvider(): Generator + { + yield from [ + [true, ['married' => '0', 'partner_name' => '']], + [true, ['married' => '1', 'partner_name' => 'Foo']], + [false, ['married' => '1', 'partner_name' => '']], + ]; + } + /** * @dataProvider requiredWithoutProvider */ diff --git a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php index e1336ac70f8b..5083bec69fe8 100644 --- a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php +++ b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Validation\Validation; use Config\Database; use Config\Services; +use InvalidArgumentException; use Tests\Support\Validation\TestRules; /** @@ -82,7 +83,17 @@ public function testIsUniqueTrue(): void $this->assertTrue($this->validation->run($data)); } - public function testIsUniqueIgnoresParams(): void + public function testIsUniqueWithInvalidDBGroup(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('invalidGroup is not a valid database connection group'); + + $this->validation->setRules(['email' => 'is_unique[user.email]']); + $data = ['email' => 'derek@world.co.uk']; + $this->assertTrue($this->validation->run($data, null, 'invalidGroup')); + } + + public function testIsUniqueWithIgnoreValue(): void { $db = Database::connect(); $db @@ -102,7 +113,7 @@ public function testIsUniqueIgnoresParams(): void $this->assertTrue($this->validation->run($data)); } - public function testIsUniqueIgnoresParamsPlaceholders(): void + public function testIsUniqueWithIgnoreValuePlaceholder(): void { $this->hasInDatabase('user', [ 'name' => 'Derek', diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index c93591cdfd07..f1d8427dc42b 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.3.6 v4.3.5 v4.3.4 v4.3.3 diff --git a/user_guide_src/source/changelogs/v4.3.5.rst b/user_guide_src/source/changelogs/v4.3.5.rst index 152f27f33a9c..0e5ab12a2fb1 100644 --- a/user_guide_src/source/changelogs/v4.3.5.rst +++ b/user_guide_src/source/changelogs/v4.3.5.rst @@ -21,11 +21,11 @@ SECURITY Changes ******* -- **make:cell** When creating a new cell, the controller would always have the ``Cell`` suffixed to the class name. - For the view file, the final ``_cell`` is always removed. -- **Cells** For compatibility with previous versions, view filenames ending with ``_cell`` can still be - located by the ``Cell`` as long as auto-detection of view file is enabled (via setting the ``$view`` property - to an empty string). +- **make:cell command:** When creating a new cell, the controller would always have the ``Cell`` suffixed to the class name. + For the view file, the final ``_cell`` is always removed. +- **View Cells:** For compatibility with previous versions, view filenames ending with ``_cell`` can still be + located by the ``Cell`` as long as auto-detection of view file is enabled (via setting the ``$view`` property + to an empty string). Deprecations ************ @@ -37,8 +37,8 @@ Bugs Fixed ********** - **Validation:** Fixed a bug where a closure used in combination with ``permit_empty`` or ``if_exist`` rules was causing an error. -- **make:cell** Fixed generating view files as classes. -- **make:cell** Fixed treatment of single word class input for case-insensitive OS. +- **make:cell command:** Fixed generating view files as classes. +- **make:cell command:** Fixed treatment of single word class input for case-insensitive OS. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/changelogs/v4.3.6.rst b/user_guide_src/source/changelogs/v4.3.6.rst new file mode 100644 index 000000000000..fb4538a7e957 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.3.6.rst @@ -0,0 +1,61 @@ +Version 4.3.6 +############# + +Release Date: June 18, 2023 + +**4.3.6 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +BREAKING +******** + +Interface Changes +================= + +.. note:: As long as you have not extended the relevant CodeIgniter core classes + or implemented these interfaces, all these changes are backward compatible + and require no intervention. + +AutoRouterInterface +------------------- + +Now ``AutoRouterInterface::getRoute()`` has the new second parameter ``string $httpVerb``. + +ValidationInterface::check() +---------------------------- + +- The second parameter has changed from ``string $rule`` to ``$rules``. +- The optional fourth parameter ``$dbGroup = null`` has been added. + +Method Signature Changes +======================== + +Validation::check() +------------------- + +- The second parameter has changed from ``string $rule`` to ``$rules``. +- The optional fourth parameter ``$dbGroup = null`` has been added. + +Deprecations +************ + +- **AutoRouterImproved:** The constructor parameter ``$httpVerb`` is deprecated. + No longer used. + +Bugs Fixed +********** + +- **Validation:** Fixed a bug that ``$DBGroup`` is ignored when checking + the value of a placeholder. +- **Validation:** Fixed a bug that ``check()`` cannot specify non-default + database group. +- **Database:** Fixed a bug where semicolon character (``;``) in one of the Postgre connection parameters would break the DSN string. +- **AutoRouting Improved:** Fixed a bug that feature testing may not find + controller/method. + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/cli/cli_commands.rst b/user_guide_src/source/cli/cli_commands.rst index 3b500cd1531a..dff328c78d6a 100644 --- a/user_guide_src/source/cli/cli_commands.rst +++ b/user_guide_src/source/cli/cli_commands.rst @@ -33,8 +33,8 @@ The following properties should be used in order to get listed in CLI commands a File Location ============= -Commands must be stored within a directory named **Commands**. However, that directory can be located anywhere -that the :doc:`Autoloader ` can locate it. This could be in **app/Commands**, or +Commands must be stored within a directory named **Commands**. However, that directory has to be located in the PSR-4 namespaces +so that the :doc:`Autoloader ` can locate it. This could be in **app/Commands**, or a directory that you keep commands in to use in all of your project development, like **Acme/Commands**. .. note:: When the commands are executed, the full CodeIgniter CLI environment has been loaded, making it @@ -49,7 +49,7 @@ should contain the following code: .. literalinclude:: cli_commands/002.php -If you run the **list** command, you will see the new command listed under its own ``demo`` group. If you take +If you run the **list** command, you will see the new command listed under its own ``Demo`` group. If you take a close look, you should see how this works fairly easily. The ``$group`` property simply tells it how to organize this command with all of the other commands that exist, telling it what heading to list it under. @@ -92,7 +92,7 @@ For example, ``return EXIT_ERROR;`` This approach can help with debugging at the system level, if the command, for example, is run via crontab. -You can use the ``EXIT_*`` exit code constants defined in the ``app/Config/Constants.php`` file. +You can use the ``EXIT_*`` exit code constants defined in the **app/Config/Constants.php** file. *********** BaseCommand @@ -127,11 +127,25 @@ be familiar with when creating your own commands. It also has a :doc:`Logger $value array. :param integer $pad: The pad spaces. - A method to calculate padding for ``$key => $value`` array output. The padding can be used to output a will formatted table in CLI: - - .. literalinclude:: cli_commands/007.php + A method to calculate padding for ``$key => $value`` array output. The padding can be used to output a will formatted table in CLI. diff --git a/user_guide_src/source/cli/cli_commands/002.php b/user_guide_src/source/cli/cli_commands/002.php index aa5d12a4438e..e2259707ae8a 100644 --- a/user_guide_src/source/cli/cli_commands/002.php +++ b/user_guide_src/source/cli/cli_commands/002.php @@ -7,7 +7,7 @@ class AppInfo extends BaseCommand { - protected $group = 'demo'; + protected $group = 'Demo'; protected $name = 'app:info'; protected $description = 'Displays basic application information.'; diff --git a/user_guide_src/source/cli/cli_commands/007.php b/user_guide_src/source/cli/cli_commands/007.php index 76150e4d4e5c..ebc4e98a6168 100644 --- a/user_guide_src/source/cli/cli_commands/007.php +++ b/user_guide_src/source/cli/cli_commands/007.php @@ -1,12 +1,15 @@ getPad($this->options, 6); +use CodeIgniter\CLI\CLI; + +$length = max(array_map('strlen', array_keys($this->options))); foreach ($this->options as $option => $description) { - CLI::write($tab . CLI::color(str_pad($option, $pad), 'green') . $description, 'yellow'); + CLI::write(CLI::color($this->setPad($option, $length, 2, 2), 'green') . $description); } /* * Output will be: - * -n Set migration namespace - * -r override file + * -n Set migration namespace + * -g Set database group + * --all Set for all namespaces, will ignore (-n) option */ diff --git a/user_guide_src/source/cli/cli_controllers.rst b/user_guide_src/source/cli/cli_controllers.rst index a46343c31aab..c8096e785c24 100644 --- a/user_guide_src/source/cli/cli_controllers.rst +++ b/user_guide_src/source/cli/cli_controllers.rst @@ -6,6 +6,11 @@ As well as calling an application's :doc:`Controllers ` via the URL in a browser they can also be loaded via the command-line interface (CLI). +.. note:: It is recommended to use Spark Commands for CLI scripts instead of + calling controllers via CLI. + See the :doc:`spark_commands` and :doc:`cli_commands` page for detailed + information. + .. contents:: :local: :depth: 2 @@ -85,6 +90,3 @@ If you want to make sure running via CLI, check the return value of :php:func:`i However, CodeIgniter provides additional tools to make creating CLI-accessible scripts even more pleasant, include CLI-only routing, and a library that helps you with CLI-only tools. - -.. note:: It is recommended to use Spark Commands for CLI scripts instead of calling controllers via CLI. - See the :doc:`spark_commands` page for detailed information. \ No newline at end of file diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index 63748f794fb2..9d86d441c660 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -26,7 +26,7 @@ version = '4.3' # The full version, including alpha/beta/rc tags. -release = '4.3.5' +release = '4.3.6' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index f9e5749ed548..ae0605d0a81d 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -4,7 +4,7 @@ Database Configuration .. contents:: :local: - :depth: 2 + :depth: 3 .. note:: See :ref:`requirements-supported-databases` for currently supported database drivers. @@ -18,6 +18,9 @@ connection values (username, password, database name, etc.). The config file is located at **app/Config/Database.php**. You can also set database connection values in the **.env** file. See below for more details. +Setting Default Database +======================== + The config settings are stored in a class property that is an array with this prototype: @@ -26,37 +29,45 @@ prototype: The name of the class property is the connection name, and can be used while connecting to specify a group name. -.. note:: The default location of the SQLite3 database is in the ``writable`` folder. +.. note:: The default location of the SQLite3 database is in the **writable** folder. If you want to change the location, you must set the full path to the new folder. DSN -=== +--- + +Some database drivers (such as Postgre, OCI8) requires a full DSN string to connect. +But if you do not specify a DSN string for a driver that requires it, CodeIgniter +will try to build it with the rest of the provided settings. -Some database drivers (such as PDO, PostgreSQL, Oracle, ODBC) might -require a full DSN string to be provided. If that is the case, you -should use the 'DSN' configuration setting, as if you're using the -driver's underlying native PHP extension, like this: +If you specify a DSN, you should use the ``'DSN'`` configuration setting, as if +you're using the driver's underlying native PHP extension, like this: .. literalinclude:: configuration/002.php + :lines: 11-15 -.. note:: If you do not specify a DSN string for a driver that requires it, CodeIgniter - will try to build it with the rest of the provided settings. +DSN in Universal Manner +^^^^^^^^^^^^^^^^^^^^^^^ You can also set a Data Source Name in universal manner (URL like). In that case DSNs must have this prototype: .. literalinclude:: configuration/003.php + :lines: 11-14 To override default config values when connecting with a universal version of the DSN string, add the config variables as a query string: .. literalinclude:: configuration/004.php + :lines: 11-15 + +.. literalinclude:: configuration/010.php + :lines: 11-15 .. note:: If you provide a DSN string and it is missing some valid settings (e.g., the database character set), which are present in the rest of the configuration fields, CodeIgniter will append them. Failovers -========= +--------- You can also specify failovers for the situation when the main connection cannot connect for some reason. These failovers can be specified by setting the failover for a connection like this: @@ -65,6 +76,9 @@ These failovers can be specified by setting the failover for a connection like t You can specify as many failovers as you like. +Setting Multiple Databases +========================== + You may optionally store multiple sets of connection values. If, for example, you run multiple environments (development, production, test, etc.) under a single installation, you can set up a @@ -78,15 +92,15 @@ variable located in the config file: .. literalinclude:: configuration/007.php -.. note:: The name 'test' is arbitrary. It can be anything you want. By - default we've used the word "default" for the primary connection, +.. note:: The name ``test`` is arbitrary. It can be anything you want. By + default we've used the word ``default`` for the primary connection, but it too can be renamed to something more relevant to your project. -defaultGroup -============ +Changing Databases Automatically +================================ You could modify the config file to detect the environment and automatically -update the `defaultGroup` value to the correct one by adding the required logic +update the ``defaultGroup`` value to the correct one by adding the required logic within the class' constructor: .. literalinclude:: configuration/008.php @@ -125,7 +139,7 @@ Explanation of Values: =============== =========================================================================================================== Name Config Description =============== =========================================================================================================== -**dsn** The DSN connect string (an all-in-one configuration sequence). +**DSN** The DSN connect string (an all-in-one configuration sequence). **hostname** The hostname of your database server. Often this is 'localhost'. **username** The username used to connect to the database. (``SQLite3`` does not use this.) **password** The password used to connect to the database. (``SQLite3`` does not use this.) diff --git a/user_guide_src/source/database/configuration/001.php b/user_guide_src/source/database/configuration/001.php index 6531870602d2..b4336abbf1c0 100644 --- a/user_guide_src/source/database/configuration/001.php +++ b/user_guide_src/source/database/configuration/001.php @@ -6,7 +6,9 @@ class Database extends Config { - public $default = [ + // ... + + public array $default = [ 'DSN' => '', 'hostname' => 'localhost', 'username' => 'root', @@ -14,7 +16,7 @@ class Database extends Config 'database' => 'database_name', 'DBDriver' => 'MySQLi', 'DBPrefix' => '', - 'pConnect' => true, + 'pConnect' => false, 'DBDebug' => true, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', diff --git a/user_guide_src/source/database/configuration/002.php b/user_guide_src/source/database/configuration/002.php index 7f9f9dabc62b..10551acdc509 100644 --- a/user_guide_src/source/database/configuration/002.php +++ b/user_guide_src/source/database/configuration/002.php @@ -1,13 +1,18 @@ 'pgsql:host=localhost;port=5432;dbname=database_name', +namespace Config; + +use CodeIgniter\Database\Config; + +class Database extends Config +{ // ... -]; -// Oracle -$default = [ - 'DSN' => '//localhost/XE', + // OCI8 + public array $default = [ + 'DSN' => '//localhost/XE', + // ... + ]; + // ... -]; +} diff --git a/user_guide_src/source/database/configuration/003.php b/user_guide_src/source/database/configuration/003.php index 1c8ea93e8288..b0fc11d308f9 100644 --- a/user_guide_src/source/database/configuration/003.php +++ b/user_guide_src/source/database/configuration/003.php @@ -1,6 +1,17 @@ 'DBDriver://username:password@hostname:port/database', +namespace Config; + +use CodeIgniter\Database\Config; + +class Database extends Config +{ + // ... + + public array $default = [ + 'DSN' => 'DBDriver://username:password@hostname:port/database', + // ... + ]; + // ... -]; +} diff --git a/user_guide_src/source/database/configuration/004.php b/user_guide_src/source/database/configuration/004.php index 05681bf62a45..4b77961706da 100644 --- a/user_guide_src/source/database/configuration/004.php +++ b/user_guide_src/source/database/configuration/004.php @@ -1,13 +1,18 @@ 'MySQLi://username:password@hostname:3306/database?charset=utf8&DBCollat=utf8_general_ci', +namespace Config; + +use CodeIgniter\Database\Config; + +class Database extends Config +{ // ... -]; -// Postgre -$default = [ - 'DSN' => 'Postgre://username:password@hostname:5432/database?charset=utf8&connect_timeout=5&sslmode=1', + // MySQLi + public array $default = [ + 'DSN' => 'MySQLi://username:password@hostname:3306/database?charset=utf8&DBCollat=utf8_general_ci', + // ... + ]; + // ... -]; +} diff --git a/user_guide_src/source/database/configuration/005.php b/user_guide_src/source/database/configuration/005.php index 7e2da93f95d3..ca23aea169d9 100644 --- a/user_guide_src/source/database/configuration/005.php +++ b/user_guide_src/source/database/configuration/005.php @@ -1,36 +1,51 @@ 'localhost1', - 'username' => '', - 'password' => '', - 'database' => '', - 'DBDriver' => 'MySQLi', - 'DBPrefix' => '', - 'pConnect' => true, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - ], - [ - 'hostname' => 'localhost2', - 'username' => '', - 'password' => '', - 'database' => '', - 'DBDriver' => 'MySQLi', - 'DBPrefix' => '', - 'pConnect' => true, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - ], -]; +namespace Config; + +use CodeIgniter\Database\Config; + +class Database extends Config +{ + // ... + + public array $default = [ + // ... + 'failover' => [ + [ + 'hostname' => 'localhost1', + 'username' => '', + 'password' => '', + 'database' => '', + 'DBDriver' => 'MySQLi', + 'DBPrefix' => '', + 'pConnect' => true, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + ], + [ + 'hostname' => 'localhost2', + 'username' => '', + 'password' => '', + 'database' => '', + 'DBDriver' => 'MySQLi', + 'DBPrefix' => '', + 'pConnect' => true, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + ], + ], + // ... + ]; + + // ... +} diff --git a/user_guide_src/source/database/configuration/006.php b/user_guide_src/source/database/configuration/006.php index 4381a0a71fff..1cd5fafe421b 100644 --- a/user_guide_src/source/database/configuration/006.php +++ b/user_guide_src/source/database/configuration/006.php @@ -6,7 +6,9 @@ class Database extends Config { - public $test = [ + // ... + + public array $test = [ 'DSN' => '', 'hostname' => 'localhost', 'username' => 'root', diff --git a/user_guide_src/source/database/configuration/007.php b/user_guide_src/source/database/configuration/007.php index 97e8523fc621..7b9d57d58aca 100644 --- a/user_guide_src/source/database/configuration/007.php +++ b/user_guide_src/source/database/configuration/007.php @@ -1,3 +1,14 @@ 'Postgre://username:password@hostname:5432/database?charset=utf8&connect_timeout=5&sslmode=1', + // ... + ]; + + // ... +} diff --git a/user_guide_src/source/database/connecting.rst b/user_guide_src/source/database/connecting.rst index aa6089e52339..d30fe155295b 100644 --- a/user_guide_src/source/database/connecting.rst +++ b/user_guide_src/source/database/connecting.rst @@ -6,11 +6,18 @@ Connecting to your Database :local: :depth: 2 +Connecting to a Database +======================== + +Connecting to the Default Group +------------------------------- + You can connect to your database by adding this line of code in any function where it is needed, or in your class constructor to make the database available globally in that class. .. literalinclude:: connecting/001.php + :lines: 2- If the above function does **not** contain any information in the first parameter, it will connect to the default group specified in your database config @@ -20,18 +27,19 @@ A convenience method exists that is purely a wrapper around the above line and is provided for your convenience: .. literalinclude:: connecting/002.php + :lines: 2- Available Parameters -------------------- **\\Config\\Database::connect($group = null, bool $getShared = true): BaseConnection** -#. ``$group``: The database group name, a string that must match the config class' property name. Default value is ``$config->defaultGroup``. +#. ``$group``: The database group name, a string that must match the config class' property name. Default value is ``Config\Database::$defaultGroup``. #. ``$getShared``: true/false (boolean). Whether to return the shared connection (see Connecting to Multiple Databases below). -Manually Connecting to a Database ---------------------------------- +Connecting to Specific Group +---------------------------- The first parameter of this function can **optionally** be used to specify a particular database group from your config file. Examples: @@ -39,8 +47,9 @@ specify a particular database group from your config file. Examples: To choose a specific group from your config file you can do this: .. literalinclude:: connecting/003.php + :lines: 2- -Where group_name is the name of the connection group from your config +Where ``group_name`` is the name of the connection group from your config file. Multiple Connections to Same Database @@ -51,6 +60,7 @@ database connection every time. If you need to have a separate connection to the same database, send ``false`` as the second parameter: .. literalinclude:: connecting/004.php + :lines: 2- Connecting to Multiple Databases ================================ @@ -59,6 +69,7 @@ If you need to connect to more than one database simultaneously you can do so as follows: .. literalinclude:: connecting/005.php + :lines: 2- Note: Change the words ``group_one`` and ``group_two`` to the specific group names you are connecting to. @@ -76,6 +87,7 @@ a connection that uses your custom settings. The array passed in must be the same format as the groups are defined in the configuration file: .. literalinclude:: connecting/006.php + :lines: 2- Reconnecting / Keeping the Connection Alive =========================================== @@ -90,11 +102,13 @@ or re-establish it. does not ping the server but it closes the connection then connects again. .. literalinclude:: connecting/007.php + :lines: 2- -Manually closing the Connection +Manually Closing the Connection =============================== While CodeIgniter intelligently takes care of closing your database connections, you can explicitly close the connection. .. literalinclude:: connecting/008.php + :lines: 2- diff --git a/user_guide_src/source/database/query_builder/028.php b/user_guide_src/source/database/query_builder/028.php index 019386718cd1..141751c40f3f 100644 --- a/user_guide_src/source/database/query_builder/028.php +++ b/user_guide_src/source/database/query_builder/028.php @@ -1,7 +1,11 @@ where('advance_amount <', static fn (BaseBuilder $builder) => $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2)); +use CodeIgniter\Database\BaseBuilder; + +$builder->where('advance_amount <', static function (BaseBuilder $builder) { + $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2); +}); // Produces: WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2) // With builder directly diff --git a/user_guide_src/source/database/query_builder/031.php b/user_guide_src/source/database/query_builder/031.php index 5aa7619c0dc8..dbbff119aa55 100644 --- a/user_guide_src/source/database/query_builder/031.php +++ b/user_guide_src/source/database/query_builder/031.php @@ -1,7 +1,11 @@ whereIn('id', static fn (BaseBuilder $builder) => $builder->select('job_id')->from('users_jobs')->where('user_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->whereIn('id', static function (BaseBuilder $builder) { + $builder->select('job_id')->from('users_jobs')->where('user_id', 3); +}); // Produces: WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/033.php b/user_guide_src/source/database/query_builder/033.php index ae8ddb7ce7a4..20ebd3e682cf 100644 --- a/user_guide_src/source/database/query_builder/033.php +++ b/user_guide_src/source/database/query_builder/033.php @@ -1,7 +1,11 @@ orWhereIn('id', static fn (BaseBuilder $builder) => $builder->select('job_id')->from('users_jobs')->where('user_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->orWhereIn('id', static function (BaseBuilder $builder) { + $builder->select('job_id')->from('users_jobs')->where('user_id', 3); +}); // Produces: OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/035.php b/user_guide_src/source/database/query_builder/035.php index 02904a3962f3..2c7a2e51bec6 100644 --- a/user_guide_src/source/database/query_builder/035.php +++ b/user_guide_src/source/database/query_builder/035.php @@ -1,7 +1,11 @@ whereNotIn('id', static fn (BaseBuilder $builder) => $builder->select('job_id')->from('users_jobs')->where('user_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->whereNotIn('id', static function (BaseBuilder $builder) { + $builder->select('job_id')->from('users_jobs')->where('user_id', 3); +}); // Produces: WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/037.php b/user_guide_src/source/database/query_builder/037.php index f5668aa1a30d..0eacfbe3a8b3 100644 --- a/user_guide_src/source/database/query_builder/037.php +++ b/user_guide_src/source/database/query_builder/037.php @@ -1,7 +1,11 @@ orWhereNotIn('id', static fn (BaseBuilder $builder) => $builder->select('job_id')->from('users_jobs')->where('user_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->orWhereNotIn('id', static function (BaseBuilder $builder) { + $builder->select('job_id')->from('users_jobs')->where('user_id', 3); +}); // Produces: OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/041.php b/user_guide_src/source/database/query_builder/041.php index 0583a2a79187..f6b13abbb4a0 100644 --- a/user_guide_src/source/database/query_builder/041.php +++ b/user_guide_src/source/database/query_builder/041.php @@ -2,4 +2,8 @@ $array = ['title' => $match, 'page1' => $match, 'page2' => $match]; $builder->like($array); -// WHERE `title` LIKE '%match%' ESCAPE '!' AND `page1` LIKE '%match%' ESCAPE '!' AND `page2` LIKE '%match%' ESCAPE '!' +/* + * WHERE `title` LIKE '%match%' ESCAPE '!' + * AND `page1` LIKE '%match%' ESCAPE '!' + * AND `page2` LIKE '%match%' ESCAPE '!' + */ diff --git a/user_guide_src/source/database/query_builder/052.php b/user_guide_src/source/database/query_builder/052.php index 1f5736243866..08ae4dca5354 100644 --- a/user_guide_src/source/database/query_builder/052.php +++ b/user_guide_src/source/database/query_builder/052.php @@ -1,7 +1,11 @@ havingIn('id', static fn (BaseBuilder $builder) => $builder->select('user_id')->from('users_jobs')->where('group_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->havingIn('id', static function (BaseBuilder $builder) { + $builder->select('user_id')->from('users_jobs')->where('group_id', 3); +}); // Produces: HAVING "id" IN (SELECT "user_id" FROM "users_jobs" WHERE "group_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/054.php b/user_guide_src/source/database/query_builder/054.php index 8a19ff4ac7d8..2594b111d667 100644 --- a/user_guide_src/source/database/query_builder/054.php +++ b/user_guide_src/source/database/query_builder/054.php @@ -1,7 +1,11 @@ orHavingIn('id', static fn (BaseBuilder $builder) => $builder->select('user_id')->from('users_jobs')->where('group_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->orHavingIn('id', static function (BaseBuilder $builder) { + $builder->select('user_id')->from('users_jobs')->where('group_id', 3); +}); // Produces: OR "id" IN (SELECT "user_id" FROM "users_jobs" WHERE "group_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/056.php b/user_guide_src/source/database/query_builder/056.php index 67a54f87f7bc..7b68c688d0df 100644 --- a/user_guide_src/source/database/query_builder/056.php +++ b/user_guide_src/source/database/query_builder/056.php @@ -1,7 +1,11 @@ havingNotIn('id', static fn (BaseBuilder $builder) => $builder->select('user_id')->from('users_jobs')->where('group_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->havingNotIn('id', static function (BaseBuilder $builder) { + $builder->select('user_id')->from('users_jobs')->where('group_id', 3); +}); // Produces: HAVING "id" NOT IN (SELECT "user_id" FROM "users_jobs" WHERE "group_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/058.php b/user_guide_src/source/database/query_builder/058.php index 8a92da5725b2..b93ab3ae9a9c 100644 --- a/user_guide_src/source/database/query_builder/058.php +++ b/user_guide_src/source/database/query_builder/058.php @@ -1,7 +1,11 @@ orHavingNotIn('id', static fn (BaseBuilder $builder) => $builder->select('user_id')->from('users_jobs')->where('group_id', 3)); +use CodeIgniter\Database\BaseBuilder; + +$builder->orHavingNotIn('id', static function (BaseBuilder $builder) { + $builder->select('user_id')->from('users_jobs')->where('group_id', 3); +}); // Produces: OR "id" NOT IN (SELECT "user_id" FROM "users_jobs" WHERE "group_id" = 3) // With builder directly diff --git a/user_guide_src/source/database/query_builder/062.php b/user_guide_src/source/database/query_builder/062.php index ba1767f6706e..08753407c447 100644 --- a/user_guide_src/source/database/query_builder/062.php +++ b/user_guide_src/source/database/query_builder/062.php @@ -2,4 +2,8 @@ $array = ['title' => $match, 'page1' => $match, 'page2' => $match]; $builder->havingLike($array); -// HAVING `title` LIKE '%match%' ESCAPE '!' AND `page1` LIKE '%match%' ESCAPE '!' AND `page2` LIKE '%match%' ESCAPE '!' +/* + * HAVING `title` LIKE '%match%' ESCAPE '!' + * AND `page1` LIKE '%match%' ESCAPE '!' + * AND `page2` LIKE '%match%' ESCAPE '!' + */ diff --git a/user_guide_src/source/database/query_builder/081.php b/user_guide_src/source/database/query_builder/081.php index 0f263005c18e..812c32194521 100644 --- a/user_guide_src/source/database/query_builder/081.php +++ b/user_guide_src/source/database/query_builder/081.php @@ -14,4 +14,9 @@ ]; $builder->insertBatch($data); -// Produces: INSERT INTO mytable (title, name, date) VALUES ('My title', 'My name', 'My date'), ('Another title', 'Another name', 'Another date') +/* + * Produces: + * INSERT INTO mytable (title, name, date) + * VALUES ('My title', 'My name', 'My date'), + * ('Another title', 'Another name', 'Another date') + */ diff --git a/user_guide_src/source/database/query_builder/098.php b/user_guide_src/source/database/query_builder/098.php index 50d274d63a54..026c4b204c5d 100644 --- a/user_guide_src/source/database/query_builder/098.php +++ b/user_guide_src/source/database/query_builder/098.php @@ -1,6 +1,6 @@ select(['field1', 'field2']) ->where('field3', 5) ->getCompiledSelect(false); @@ -13,5 +13,5 @@ $data = $builder->get()->getResultArray(); /* * Would execute and return an array of results of the following query: - * SELECT field1, field1 from mytable where field3 = 5; + * SELECT field1, field2 FROM mytable WHERE field3 = 5; */ diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index c2d72bdab746..a5d195f53e46 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -4,7 +4,7 @@ URI Routing .. contents:: :local: - :depth: 2 + :depth: 3 What is URI Routing? ******************** @@ -31,7 +31,8 @@ If you expect a GET request, you use the ``get()`` method: .. literalinclude:: routing/001.php -A route takes the URI path (``/``) on the left, and maps it to the controller and method (``Home::index``) on the right, +A route takes the **Route Path** (URI path relative to the BaseURL. ``/``) on the left, +and maps it to the **Route Handler** (controller and method ``Home::index``) on the right, along with any parameters that should be passed to the controller. The controller and method should @@ -68,8 +69,8 @@ and the ``productLookupByID()`` method passing in the match as a variable to the .. literalinclude:: routing/009.php -HTTP verbs -========== +HTTP verb Routes +================ You can use any standard HTTP verb (GET, POST, PUT, DELETE, OPTIONS, etc): @@ -79,10 +80,15 @@ You can supply multiple verbs that a route should match by passing them in as an .. literalinclude:: routing/004.php +Specifying Route Handlers +========================= + Controller's Namespace -====================== +---------------------- -If a controller name is stated without beginning with ``\``, the :ref:`routing-default-namespace` will be prepended: +When you specify a controller and method name as a string, if a controller is +written without a leading ``\``, the :ref:`routing-default-namespace` will be +prepended: .. literalinclude:: routing/063.php @@ -96,8 +102,57 @@ You can also specify the namespace with the ``namespace`` option: See :ref:`assigning-namespace` for details. +Array Callable Syntax +--------------------- + +.. versionadded:: 4.2.0 + +Since v4.2.0, you can use array callable syntax to specify the controller: + +.. literalinclude:: routing/013.php + :lines: 2- + +Or using ``use`` keyword: + +.. literalinclude:: routing/014.php + :lines: 2- + +If you forget to add ``use App\Controllers\Home;``, the controller classname is +interpreted as ``Config\Home``, not ``App\Controllers\Home`` because +**app/Config/Routes.php** has ``namespace Config;`` at the top. + +.. note:: When you use Array Callable Syntax, the classname is always interpreted + as a fully qualified classname. So :ref:`routing-default-namespace` and + :ref:`namespace option ` have no effect. + +Array Callable Syntax and Placeholders +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If there are placeholders, it will automatically set the parameters in the specified order: + +.. literalinclude:: routing/015.php + :lines: 2- + +But the auto-configured parameters may not be correct if you use regular expressions in routes. +In such a case, you can specify the parameters manually: + +.. literalinclude:: routing/016.php + :lines: 2- + +Using Closures +-------------- + +You can use an anonymous function, or Closure, as the destination that a route maps to. This function will be +executed when the user visits that URI. This is handy for quickly executing small tasks, or even just showing +a simple view: + +.. literalinclude:: routing/020.php + +Specifying Route Paths +====================== + Placeholders -============ +------------ A typical route might look something like this: @@ -147,35 +202,8 @@ routes. With the examples URLs from above: will only match **product/123** and generate 404 errors for other example. - -Array Callable Syntax -===================== - -.. versionadded:: 4.2.0 - -Since v4.2.0, you can use array callable syntax to specify the controller: - -.. literalinclude:: routing/013.php - :lines: 2- - -Or using ``use`` keyword: - -.. literalinclude:: routing/014.php - :lines: 2- - -If there are placeholders, it will automatically set the parameters in the specified order: - -.. literalinclude:: routing/015.php - :lines: 2- - -But the auto-configured parameters may not be correct if you use regular expressions in routes. -In such a case, you can specify the parameters manually: - -.. literalinclude:: routing/016.php - :lines: 2- - Custom Placeholders -=================== +------------------- You can create your own placeholders that can be used in your routes file to fully customize the experience and readability. @@ -187,7 +215,7 @@ This must be called before you add the route: .. literalinclude:: routing/017.php Regular Expressions -=================== +------------------- If you prefer you can use regular expressions to define your routing rules. Any valid regular expression is allowed, as are back-references. @@ -213,19 +241,10 @@ For those of you who don't know regular expressions and want to learn more about .. note:: You can also mix and match placeholders with regular expressions. -Closures -======== - -You can use an anonymous function, or Closure, as the destination that a route maps to. This function will be -executed when the user visits that URI. This is handy for quickly executing small tasks, or even just showing -a simple view: - -.. literalinclude:: routing/020.php - .. _view-routes: -Views -===== +View Routes +=========== .. versionadded:: 4.3.0 @@ -258,45 +277,6 @@ redirect and is recommended in most cases: If a redirect route is matched during a page load, the user will be immediately redirected to the new page before a controller can be loaded. -Grouping Routes -=============== - -You can group your routes under a common name with the ``group()`` method. The group name becomes a segment that -appears prior to the routes defined inside of the group. This allows you to reduce the typing needed to build out an -extensive set of routes that all share the opening string, like when building an admin area: - -.. literalinclude:: routing/023.php - -This would prefix the **users** and **blog** URIs with **admin**, handling URLs like **admin/users** and **admin/blog**. - -If you need to assign options to a group, like a :ref:`assigning-namespace`, do it before the callback: - -.. literalinclude:: routing/024.php - -This would handle a resource route to the ``App\API\v1\Users`` controller with the **api/users** URI. - -You can also use a specific :doc:`filter ` for a group of routes. This will always -run the filter before or after the controller. This is especially handy during authentication or api logging: - -.. literalinclude:: routing/025.php - -The value for the filter must match one of the aliases defined within **app/Config/Filters.php**. - -It is possible to nest groups within groups for finer organization if you need it: - -.. literalinclude:: routing/026.php - -This would handle the URL at **admin/users/list**. - -.. note:: Options passed to the outer ``group()`` (for example ``namespace`` and ``filter``) are not merged with the inner ``group()`` options. - -At some point, you may want to group routes for the purpose of applying filters or other route -config options like namespace, subdomain, etc. Without necessarily needing to add a prefix to the group, you can pass -an empty string in place of the prefix and the routes in the group will be routed as though the group never existed but with the -given route config options: - -.. literalinclude:: routing/027.php - Environment Restrictions ======================== @@ -307,36 +287,6 @@ routes defined within this closure are only accessible from the given environmen .. literalinclude:: routing/028.php -.. _reverse-routing: - -Reverse Routing -=============== - -Reverse routing allows you to define the controller and method, as well as any parameters, that a link should go -to, and have the router lookup the current route to it. This allows route definitions to change without you having -to update your application code. This is typically used within views to create links. - -For example, if you have a route to a photo gallery that you want to link to, you can use the :php:func:`url_to()` helper -function to get the route that should be used. The first parameter is the fully qualified Controller and method, -separated by a double colon (``::``), much like you would use when writing the initial route itself. Any parameters that -should be passed to the route are passed in next: - -.. literalinclude:: routing/029.php - -.. _using-named-routes: - -Using Named Routes -================== - -You can name routes to make your application less fragile. This applies a name to a route that can be called -later, and even if the route definition changes, all of the links in your application built with :php:func:`url_to()` -will still work without you having to make any changes. A route is named by passing in the ``as`` option -with the name of the route: - -.. literalinclude:: routing/030.php - -This has the added benefit of making the views more readable, too. - Routes with any HTTP verbs ========================== @@ -371,6 +321,9 @@ define an array of routes and then pass it as the first parameter to the ``map() 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 route methods will also be inaccessible from the CLI, but routes created by the ``add()`` method will still be @@ -378,19 +331,13 @@ available from the command line: .. literalinclude:: routing/032.php -.. 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. - .. warning:: If you enable :ref:`auto-routing-legacy` and place the command file in **app/Controllers**, anyone could access the command with the help of Auto Routing (Legacy) via HTTP. -.. note:: It is recommended to use Spark Commands instead of CLI routes. - See the :doc:`../cli/spark_commands` page for detailed information. - Global Options -============== +************** -All of the methods for creating a route (add, get, post, :doc:`resource ` etc) can take an array of options that +All of the methods for creating a route (``get()``, ``post()``, :doc:`resource() ` etc) can take an array of options that can modify the generated routes, or further restrict them. The ``$options`` array is always the last parameter: .. literalinclude:: routing/033.php @@ -398,7 +345,7 @@ can modify the generated routes, or further restrict them. The ``$options`` arra .. _applying-filters: Applying Filters ----------------- +================ You can alter the behavior of specific routes by supplying filters to run before or after the controller. This is especially handy during authentication or api logging. The value for the filter can be a string or an array of strings: @@ -416,7 +363,7 @@ See :doc:`Controller filters ` for more information on setting up filte See :ref:`use-defined-routes-only` to disable auto-routing. Alias Filter -^^^^^^^^^^^^ +------------ You specify an alias defined in **app/Config/Filters.php** for the filter value: @@ -427,7 +374,7 @@ You may also supply arguments to be passed to the alias filter's ``before()`` an .. literalinclude:: routing/035.php Classname Filter -^^^^^^^^^^^^^^^^ +---------------- .. versionadded:: 4.1.5 @@ -436,7 +383,7 @@ You specify a filter classname for the filter value: .. literalinclude:: routing/036.php Multiple Filters -^^^^^^^^^^^^^^^^ +---------------- .. versionadded:: 4.1.5 @@ -449,7 +396,7 @@ You specify an array for the filter value: .. _assigning-namespace: Assigning Namespace -------------------- +=================== While a :ref:`routing-default-namespace` will be prepended to the generated controllers, you can also specify a different namespace to be used in any options array, with the ``namespace`` option. The value should be the @@ -462,7 +409,7 @@ For any methods that create multiple routes, the new namespace is attached to al or, in the case of ``group()``, all routes generated while in the closure. Limit to Hostname ------------------ +================= You can restrict groups of routes to function only in certain domain or sub-domains of your application by passing the "hostname" option along with the desired domain to allow it on as part of the options array: @@ -473,7 +420,7 @@ This example would only allow the specified hosts to work if the domain exactly It would not work under the main site at **example.com**. Limit to Subdomains -------------------- +=================== When the ``subdomain`` option is present, the system will restrict the routes to only be available on that sub-domain. The route will only be matched if the subdomain is the one the application is being viewed through: @@ -490,7 +437,7 @@ that does not have any subdomain present, this will not be matched: to separate suffixes or www) can potentially lead to false positives. Offsetting the Matched Parameters ---------------------------------- +================================= You can offset the matched parameters in your route by any numeric value with the ``offset`` option, with the value being the number of segments to offset. @@ -500,6 +447,87 @@ be used when the first parameter is a language string: .. literalinclude:: routing/042.php +.. _reverse-routing: + +Reverse Routing +*************** + +Reverse routing allows you to define the controller and method, as well as any parameters, that a link should go +to, and have the router lookup the current route to it. This allows route definitions to change without you having +to update your application code. This is typically used within views to create links. + +For example, if you have a route to a photo gallery that you want to link to, you can use the :php:func:`url_to()` helper +function to get the route that should be used. The first parameter is the fully qualified Controller and method, +separated by a double colon (``::``), much like you would use when writing the initial route itself. Any parameters that +should be passed to the route are passed in next: + +.. literalinclude:: routing/029.php + +.. _using-named-routes: + +Named Routes +************ + +You can name routes to make your application less fragile. This applies a name to a route that can be called +later, and even if the route definition changes, all of the links in your application built with :php:func:`url_to()` +will still work without you having to make any changes. A route is named by passing in the ``as`` option +with the name of the route: + +.. literalinclude:: routing/030.php + +This has the added benefit of making the views more readable, too. + +Grouping Routes +*************** + +You can group your routes under a common name with the ``group()`` method. The group name becomes a segment that +appears prior to the routes defined inside of the group. This allows you to reduce the typing needed to build out an +extensive set of routes that all share the opening string, like when building an admin area: + +.. literalinclude:: routing/023.php + +This would prefix the **users** and **blog** URIs with **admin**, handling URLs like **admin/users** and **admin/blog**. + +Setting Namespace +================= + +If you need to assign options to a group, like a :ref:`assigning-namespace`, do it before the callback: + +.. literalinclude:: routing/024.php + +This would handle a resource route to the ``App\API\v1\Users`` controller with the **api/users** URI. + +Setting Filters +=============== + +You can also use a specific :doc:`filter ` for a group of routes. This will always +run the filter before or after the controller. This is especially handy during authentication or api logging: + +.. literalinclude:: routing/025.php + +The value for the filter must match one of the aliases defined within **app/Config/Filters.php**. + +Setting Other Options +===================== + +At some point, you may want to group routes for the purpose of applying filters or other route +config options like namespace, subdomain, etc. Without necessarily needing to add a prefix to the group, you can pass +an empty string in place of the prefix and the routes in the group will be routed as though the group never existed but with the +given route config options: + +.. literalinclude:: routing/027.php + +Nesting Groups +============== + +It is possible to nest groups within groups for finer organization if you need it: + +.. literalinclude:: routing/026.php + +This would handle the URL at **admin/users/list**. + +.. note:: Options passed to the outer ``group()`` (for example ``namespace`` and ``filter``) are not merged with the inner ``group()`` options. + .. _routing-priority: Route Priority @@ -507,7 +535,7 @@ Route Priority Routes are registered in the routing table in the order in which they are defined. This means that when a URI is accessed, the first matching route will be executed. -.. note:: If a route (the URI path) is defined more than once with different handlers, only the first defined route is registered. +.. warning:: If a route path is defined more than once with different handlers, only the first defined route is registered. You can check registered routes in the routing table by running the :ref:`spark routes ` command. @@ -804,8 +832,8 @@ CodeIgniter has the following :doc:`command ` to display al .. _routing-spark-routes: -routes -====== +spark routes +============ Displays all routes and filters:: @@ -824,9 +852,9 @@ The output is like the following: The *Method* column shows the HTTP method that the route is listening for. -The *Route* column shows the route (URI path) to match. The route of a defined route is expressed as a regular expression. +The *Route* column shows the route path to match. The route of a defined route is expressed as a regular expression. -Since v4.3.0, the *Name* column shows the route name. ``»`` indicates the name is the same as the route. +Since v4.3.0, the *Name* column shows the route name. ``»`` indicates the name is the same as the route path. .. important:: The system is not perfect. If you use Custom Placeholders, *Filters* might not be correct. If you want to check filters for a route, you can use :ref:`spark filter:check ` command. @@ -847,6 +875,8 @@ The *Method* will be like ``GET(auto)``. ``/..`` in the *Route* column indicates one segment. ``[/..]`` indicates it is optional. +.. note:: When auto-routing is enabled and you have the route ``home``, it can be also accessed by ``Home``, or maybe by ``hOme``, ``hoMe``, ``HOME``, etc. but the command will show only ``home``. + If you see a route starting with ``x`` like the following, it indicates an invalid route that won't be routed, but the controller has a public method for routing. @@ -883,7 +913,7 @@ The *Method* will be ``auto``. ``[/...]`` in the *Route* column indicates any number of segments. -.. note:: When auto-routing is enabled, if you have the route ``home``, it can be also accessd by ``Home``, or maybe by ``hOme``, ``hoMe``, ``HOME``, etc. But the command shows only ``home``. +.. note:: When auto-routing is enabled and you have the route ``home``, it can be also accessed by ``Home``, or maybe by ``hOme``, ``hoMe``, ``HOME``, etc. but the command will show only ``home``. .. _routing-spark-routes-sort-by-handler: diff --git a/user_guide_src/source/incoming/routing/008.php b/user_guide_src/source/incoming/routing/008.php index d840165c7809..0e9c3afdafa7 100644 --- a/user_guide_src/source/incoming/routing/008.php +++ b/user_guide_src/source/incoming/routing/008.php @@ -1,3 +1,3 @@ get('product/(:any)', 'Catalog::productLookup'); +$routes->get('product/(:segment)', 'Catalog::productLookup'); diff --git a/user_guide_src/source/installation/upgrade_436.rst b/user_guide_src/source/installation/upgrade_436.rst new file mode 100644 index 000000000000..cd4ff8bdeeb1 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_436.rst @@ -0,0 +1,38 @@ +############################# +Upgrading from 4.3.5 to 4.3.6 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +Breaking Changes +**************** + +- ``AutoRouterInterface::getRoute()`` has the new second parameter ``string $httpVerb``. + If you implement it, add the parameter. + +Breaking Enhancements +********************* + +- The method signatures of ``ValidationInterface::check()`` and ``Validation::check()`` + have been changed. If you implement or extend them, update the signatures. + +Project Files +************* + +Version 4.3.6 did not alter any executable code in project files. + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- composer.json diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index 4d9c1339142d..6b411b767044 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`. backward_compatibility_notes + upgrade_436 upgrade_435 upgrade_434 upgrade_433 diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 6c6ec9c5c8ce..c9baf8a8f597 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -11,7 +11,7 @@ is the ``Time`` class and lives in the ``CodeIgniter\I18n`` namespace. .. note:: Prior to v4.3.0, the Time class extended ``DateTime`` and some inherited methods changed the current object state. The bug was fixed in v4.3.0. If you need the old Time class for backward - compatibility, you can use deprecated ``TimeLegay`` class for the time being. + compatibility, you can use deprecated ``TimeLegacy`` class for the time being. .. contents:: :local: diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 5b2b58d06d2a..6142a9f9f6be 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -690,14 +690,23 @@ Your new custom rule could now be used just like any other rule: Allowing Parameters ------------------- -If your method needs to work with parameters, the function will need a minimum of three parameters: the value to validate, -the parameter string, and an array with all of the data that was submitted the form. The ``$data`` array is especially handy +If your method needs to work with parameters, the function will need a minimum of three parameters: + +1. the value to validate (``$value``) +2. the parameter string (``$params``) +3. an array with all of the data that was submitted the form (``$data``) +4. (optional) a custom error string (``&$error``), just as described above. + +.. warning:: The field values in ``$data`` are unvalidated (or may be invalid). + Using unvalidated input data is a source of vulnerability. You must + perform the necessary validation within your custom rules before using the + data in ``$data``. + +The ``$data`` array is especially handy for rules like ``required_with`` that needs to check the value of another submitted field to base its result on: .. literalinclude:: validation/037.php -Custom errors can be returned as the fourth parameter ``&$error``, just as described above. - .. _validation-using-closure-rule: Using Closure Rule @@ -750,29 +759,30 @@ alpha_numeric_space No Fails if field contains anything other than alphanumeric or space characters. alpha_numeric_punct No Fails if field contains anything other than alphanumeric, space, or this limited set of - punctuation characters: ~ (tilde), - ! (exclamation), # (number), $ (dollar), - % (percent), & (ampersand), * (asterisk), - - (dash), _ (underscore), + (plus), - = (equals), | (vertical bar), : (colon), - . (period). + punctuation characters: ``~`` (tilde), + ``!`` (exclamation), ``#`` (number), + ``$`` (dollar), ``% (percent), & (ampersand), + ``*`` (asterisk), ``-`` (dash), + ``_`` (underscore), ``+`` (plus), + ``=`` (equals), ``|`` (vertical bar), + ``:`` (colon), ``.`` (period). decimal No Fails if field contains anything other than - a decimal number. - Also accepts a + or - sign for the number. -differs Yes Fails if field does not differ from the one differs[field_name] + a decimal number. Also accepts a ``+`` or + ``-`` sign for the number. +differs Yes Fails if field does not differ from the one ``differs[field_name]`` in the parameter. -exact_length Yes Fails if field is not exactly the parameter exact_length[5] or exact_length[5,8,12] +exact_length Yes Fails if field is not exactly the parameter ``exact_length[5]`` or ``exact_length[5,8,12]`` value. One or more comma-separated values. -greater_than Yes Fails if field is less than or equal to greater_than[8] +greater_than Yes Fails if field is less than or equal to ``greater_than[8]`` the parameter value or not numeric. -greater_than_equal_to Yes Fails if field is less than the parameter greater_than_equal_to[5] +greater_than_equal_to Yes Fails if field is less than the parameter ``greater_than_equal_to[5]`` value, or not numeric. hex No Fails if field contains anything other than hexadecimal characters. if_exist No If this rule is present, validation will - only return possible errors if the field key - exists, regardless of its value. -in_list Yes Fails if field is not within a predetermined in_list[red,blue,green] + check the field only when the field key + exists in the data to validate. +in_list Yes Fails if field is not within a predetermined ``in_list[red,blue,green]`` list. integer No Fails if field contains anything other than an integer. @@ -780,41 +790,41 @@ is_natural No Fails if field contains anything other than a natural number: 0, 1, 2, 3, etc. is_natural_no_zero No Fails if field contains anything other than a natural number, except zero: 1, 2, 3, etc. -is_not_unique Yes Checks the database to see if the given value is_not_unique[table.field,where_field,where_value] +is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` exist. Can ignore records by field/value to filter (currently accept only one filter). -is_unique Yes Checks if this field value exists in the is_unique[table.field,ignore_field,ignore_value] +is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]`` database. Optionally set a column and value to ignore, useful when updating records to ignore itself. -less_than Yes Fails if field is greater than or equal to less_than[8] +less_than Yes Fails if field is greater than or equal to ``less_than[8]`` the parameter value or not numeric. -less_than_equal_to Yes Fails if field is greater than the parameter less_than_equal_to[8] +less_than_equal_to Yes Fails if field is greater than the parameter ``less_than_equal_to[8]`` value or not numeric. matches Yes The value must match the value of the field - in the parameter. matches[field] -max_length Yes Fails if field is longer than the parameter max_length[8] + in the parameter. ``matches[field]`` +max_length Yes Fails if field is longer than the parameter ``max_length[8]`` value. -min_length Yes Fails if field is shorter than the parameter min_length[3] +min_length Yes Fails if field is shorter than the parameter ``min_length[3]`` value. -not_in_list Yes Fails if field is within a predetermined not_in_list[red,blue,green] +not_in_list Yes Fails if field is within a predetermined ``not_in_list[red,blue,green]`` list. numeric No Fails if field contains anything other than numeric characters. -regex_match Yes Fails if field does not match the regular regex_match[/regex/] +regex_match Yes Fails if field does not match the regular ``regex_match[/regex/]`` expression. permit_empty No Allows the field to receive an empty array, empty string, null or false. required No Fails if the field is an empty array, empty string, null or false. -required_with Yes The field is required when any of the other required_with[field1,field2] - required fields are present in the data. -required_without Yes The field is required when any of other required_without[field1,field2] - fields do not pass ``required`` checks. +required_with Yes The field is required when any of the other ``required_with[field1,field2]`` + fields is not `empty()`_ in the data. +required_without Yes The field is required when any of the other ``required_without[field1,field2]`` + fields is `empty()`_ in the data. string No A generic alternative to the alpha* rules that confirms the element is a string timezone No Fails if field does match a timezone per - ``timezone_identifiers_list`` + `timezone_identifiers_list()`_ valid_base64 No Fails if field contains anything other than valid Base64 characters. valid_json No Fails if field does not contain a valid JSON @@ -823,45 +833,55 @@ valid_email No Fails if field does not contain a valid email address. valid_emails No Fails if any value provided in a comma separated list is not a valid email. -valid_ip No Fails if the supplied IP is not valid. valid_ip[ipv6] - Accepts an optional parameter of 'ipv4' or - 'ipv6' to specify an IP format. +valid_ip Yes Fails if the supplied IP is not valid. ``valid_ip[ipv6]`` + Accepts an optional parameter of ``ipv4`` or + ``ipv6`` to specify an IP format. valid_url No Fails if field does not contain (loosely) a URL. Includes simple strings that could be hostnames, like "codeigniter". -valid_url_strict Yes Fails if field does not contain a valid URL. valid_url_strict[https] + **Normally,** ``valid_url_strict`` **should + be used.** +valid_url_strict Yes Fails if field does not contain a valid URL. ``valid_url_strict[https]`` You can optionally specify a list of valid schemas. If not specified, ``http,https`` - are valid. This rule uses - PHP's ``FILTER_VALIDATE_URL``. -valid_date No Fails if field does not contain a valid date. valid_date[d/m/Y] - Accepts an optional parameter to matches - a date format. -valid_cc_number Yes Verifies that the credit card number matches valid_cc_number[amex] + are valid. This rule uses PHP's + ``FILTER_VALIDATE_URL``. +valid_date Yes Fails if field does not contain a valid date. ``valid_date[d/m/Y]`` + Any string that `strtotime()`_ accepts is + valid if you don't specify an optional + parameter to matches a date format. + **So it is usually necessary to specify + the parameter.** +valid_cc_number Yes Verifies that the credit card number matches ``valid_cc_number[amex]`` the format used by the specified provider. Current supported providers are: - American Express (amex), - China Unionpay (unionpay), - Diners Club CarteBlance (carteblanche), - Diners Club (dinersclub), - Discover Card (discover), - Interpayment (interpayment), JCB (jcb), - Maestro (maestro), Dankort (dankort), - NSPK MIR (mir), - Troy (troy), MasterCard (mastercard), - Visa (visa), UATP (uatp), Verve (verve), - CIBC Convenience Card (cibc), - Royal Bank of Canada Client Card (rbc), - TD Canada Trust Access Card (tdtrust), - Scotiabank Scotia Card (scotia), - BMO ABM Card (bmoabm), - HSBC Canada Card (hsbc) + American Express (``amex``), + China Unionpay (``unionpay``), + Diners Club CarteBlance (``carteblanche``), + Diners Club (``dinersclub``), + Discover Card (``discover``), + Interpayment (``interpayment``), + JCB (``jcb``), Maestro (``maestro``), + Dankort (``dankort``), NSPK MIR (``mir``), + Troy (``troy``), MasterCard (``mastercard``), + Visa (``visa``), UATP (``uatp``), + Verve (``verve``), + CIBC Convenience Card (``cibc``), + Royal Bank of Canada Client Card (``rbc``), + TD Canada Trust Access Card (``tdtrust``), + Scotiabank Scotia Card (``scotia``), + BMO ABM Card (``bmoabm``), + HSBC Canada Card (``hsbc``) ======================= ========== ============================================= =================================================== .. note:: You can also use any native PHP functions that return boolean and permit at least one parameter, the field data to validate. The Validation library **never alters the data** to validate. +.. _timezone_identifiers_list(): https://www.php.net/manual/en/function.timezone-identifiers-list.php +.. _strtotime(): https://www.php.net/manual/en/function.strtotime.php +.. _empty(): https://www.php.net/manual/en/function.empty.php + .. _rules-for-file-uploads: Rules for File Uploads @@ -883,25 +903,25 @@ file upload related rules:: ======================= ========== ============================================= =================================================== Rule Parameter Description Example ======================= ========== ============================================= =================================================== -uploaded Yes Fails if the name of the parameter does not uploaded[field_name] +uploaded Yes Fails if the name of the parameter does not ``uploaded[field_name]`` match the name of any uploaded files. -max_size Yes Fails if the uploaded file named in the max_size[field_name,2048] +max_size Yes Fails if the uploaded file named in the ``max_size[field_name,2048]`` parameter is larger than the second parameter in kilobytes (kb). Or if the file is larger than allowed maximum size declared in php.ini config file - ``upload_max_filesize`` directive. -max_dims Yes Fails if the maximum width and height of an max_dims[field_name,300,150] +max_dims Yes Fails if the maximum width and height of an ``max_dims[field_name,300,150]`` uploaded image exceed values. The first parameter is the field name. The second is the width, and the third is the height. Will also fail if the file cannot be determined to be an image. -mime_in Yes Fails if the file's mime type is not one mime_in[field_name,image/png,image/jpeg] +mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` listed in the parameters. -ext_in Yes Fails if the file's extension is not one ext_in[field_name,png,jpg,gif] +ext_in Yes Fails if the file's extension is not one ``ext_in[field_name,png,jpg,gif]`` listed in the parameters. -is_image Yes Fails if the file cannot be determined to be is_image[field_name] +is_image Yes Fails if the file cannot be determined to be ``is_image[field_name]`` an image based on the mime type. ======================= ========== ============================================= =================================================== diff --git a/user_guide_src/source/libraries/validation/038.php b/user_guide_src/source/libraries/validation/038.php index f37307b0ae07..f4ec3371527f 100644 --- a/user_guide_src/source/libraries/validation/038.php +++ b/user_guide_src/source/libraries/validation/038.php @@ -6,5 +6,7 @@ '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" + 'name' => 'is_unique[supplier.name,uuid,{uuid}]', // is ok - see "Validation Placeholders" ]); +// Warning: If `$uuid` is a user input, be sure to validate the format of the value before using it. +// Otherwise, it is vulnerable.