-
Page rendered in {elapsed_time} seconds
+
Page rendered in {elapsed_time} seconds using {memory_usage} MB of memory.
Environment: = ENVIRONMENT ?>
diff --git a/composer.json b/composer.json
index b5baf427017a..9c17f59ded32 100644
--- a/composer.json
+++ b/composer.json
@@ -10,12 +10,11 @@
"slack": "https://codeigniterchat.slack.com"
},
"require": {
- "php": "^7.4 || ^8.0",
+ "php": "^8.1",
"ext-intl": "*",
- "ext-json": "*",
"ext-mbstring": "*",
- "laminas/laminas-escaper": "^2.9",
- "psr/log": "^1.1"
+ "laminas/laminas-escaper": "^2.13",
+ "psr/log": "^3.0"
},
"require-dev": {
"codeigniter/coding-standard": "^1.7",
@@ -26,13 +25,12 @@
"kint-php/kint": "^5.0.4",
"mikey179/vfsstream": "^1.6",
"nexusphp/cs-config": "^3.6",
- "nexusphp/tachycardia": "^1.0",
- "php-coveralls/php-coveralls": "^2.5",
+ "nexusphp/tachycardia": "^2.0",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10.2",
"phpstan/phpstan-strict-rules": "^1.5",
- "phpunit/phpcov": "^8.2",
- "phpunit/phpunit": "^9.1",
+ "phpunit/phpcov": "^9.0.2",
+ "phpunit/phpunit": "^10.5.16",
"predis/predis": "^1.1 || ^2.0",
"rector/rector": "1.0.4",
"vimeo/psalm": "^5.0"
@@ -91,7 +89,8 @@
},
"scripts": {
"post-update-cmd": [
- "CodeIgniter\\ComposerScripts::postUpdate"
+ "CodeIgniter\\ComposerScripts::postUpdate",
+ "composer update --working-dir=tools/phpmetrics"
],
"analyze": [
"Composer\\Config::disableProcessTimeout",
@@ -110,6 +109,7 @@
"php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php",
"php-cs-fixer fix --ansi --verbose --diff"
],
+ "metrics": "tools/phpmetrics/vendor/bin/phpmetrics --config=phpmetrics.json",
"sa": "@analyze",
"style": "@cs-fix",
"test": "phpunit"
@@ -118,6 +118,7 @@
"analyze": "Run static analysis",
"cs": "Check the coding style",
"cs-fix": "Fix the coding style",
+ "metrics": "Run PhpMetrics",
"test": "Run unit tests"
}
}
diff --git a/contributing/internals.md b/contributing/internals.md
index e2d387028fe1..b49c9832d390 100644
--- a/contributing/internals.md
+++ b/contributing/internals.md
@@ -29,10 +29,9 @@ PHP7 provides [Type declarations](https://www.php.net/manual/en/language.types.d
for method parameters and return types. Use it where possible. Return type
declaration is not always practical, but do try to make it work.
-At this time, shipped CI4 production code does not use
-[Strict typing](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict),
-and will not be any time soon. However, in the development phase,
-there are internal classes (in `utils/`) that are strictly typed.
+At this time, shipped CI4 production code does use
+[Strict typing](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict)
+as much as possible.
## Abstractions
diff --git a/contributing/pull_request.md b/contributing/pull_request.md
index 38f32d40c1be..4b5b06406124 100644
--- a/contributing/pull_request.md
+++ b/contributing/pull_request.md
@@ -154,7 +154,7 @@ See [Contribution CSS](./css.md).
### Compatibility
-CodeIgniter4 requires [PHP 7.4](https://php.net/releases/7_4_0.php).
+CodeIgniter4 requires [PHP 8.1](https://php.net/releases/8_1_0.php).
### Backwards Compatibility
diff --git a/deptrac.yaml b/deptrac.yaml
index 271378bfde55..178de9a74fd7 100644
--- a/deptrac.yaml
+++ b/deptrac.yaml
@@ -35,6 +35,14 @@ parameters:
collectors:
- type: className
regex: ^Codeigniter\\Database\\.*
+ - name: DataCaster
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\DataCaster\\.*
+ - name: DataConverter
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\DataConverter\\.*
- name: Email
collectors:
- type: className
@@ -166,10 +174,17 @@ parameters:
- Entity
- Events
- I18n
+ DataCaster:
+ - I18n
+ - URI
+ - Database
+ DataConverter:
+ - DataCaster
Email:
- I18n
- Events
Entity:
+ - DataCaster
- I18n
Files:
- I18n
@@ -189,6 +204,7 @@ parameters:
- I18n
Model:
- Database
+ - DataConverter
- Entity
- I18n
- Pager
@@ -219,18 +235,29 @@ parameters:
- I18n
Validation:
- HTTP
+ - Database
View:
- Cache
skip_violations:
# Individual class exemptions
CodeIgniter\Cache\ResponseCache:
- CodeIgniter\HTTP\CLIRequest
+ - CodeIgniter\HTTP\Header
- CodeIgniter\HTTP\IncomingRequest
- CodeIgniter\HTTP\ResponseInterface
+ CodeIgniter\DataCaster\DataCaster:
+ - CodeIgniter\Entity\Cast\CastInterface
+ - CodeIgniter\Entity\Exceptions\CastException
+ CodeIgniter\DataCaster\Exceptions\CastException:
+ - CodeIgniter\Entity\Exceptions\CastException
+ CodeIgniter\DataConverter\DataConverter:
+ - CodeIgniter\Entity\Entity
CodeIgniter\Entity\Cast\URICast:
- CodeIgniter\HTTP\URI
CodeIgniter\Log\Handlers\ChromeLoggerHandler:
- CodeIgniter\HTTP\ResponseInterface
+ CodeIgniter\Security\CheckPhpIni:
+ - CodeIgniter\View\Table
CodeIgniter\View\Table:
- CodeIgniter\Database\BaseResult
CodeIgniter\View\Plugins:
diff --git a/env b/env
index e60354b3c4f8..f359ec20bff2 100644
--- a/env
+++ b/env
@@ -38,106 +38,32 @@
# database.default.DBPrefix =
# database.default.port = 3306
+# If you use MySQLi as tests, first update the values of Config\Database::$tests.
# database.tests.hostname = localhost
# database.tests.database = ci4_test
# database.tests.username = root
# database.tests.password = root
# database.tests.DBDriver = MySQLi
# database.tests.DBPrefix =
+# database.tests.charset = utf8mb4
+# database.tests.DBCollat = utf8mb4_general_ci
# database.tests.port = 3306
-#--------------------------------------------------------------------
-# CONTENT SECURITY POLICY
-#--------------------------------------------------------------------
-
-# contentsecuritypolicy.reportOnly = false
-# contentsecuritypolicy.defaultSrc = 'none'
-# contentsecuritypolicy.scriptSrc = 'self'
-# contentsecuritypolicy.styleSrc = 'self'
-# contentsecuritypolicy.imageSrc = 'self'
-# contentsecuritypolicy.baseURI = null
-# contentsecuritypolicy.childSrc = null
-# contentsecuritypolicy.connectSrc = 'self'
-# contentsecuritypolicy.fontSrc = null
-# contentsecuritypolicy.formAction = null
-# contentsecuritypolicy.frameAncestors = null
-# contentsecuritypolicy.frameSrc = null
-# contentsecuritypolicy.mediaSrc = null
-# contentsecuritypolicy.objectSrc = null
-# contentsecuritypolicy.pluginTypes = null
-# contentsecuritypolicy.reportURI = null
-# contentsecuritypolicy.sandbox = false
-# contentsecuritypolicy.upgradeInsecureRequests = false
-# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
-# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
-# contentsecuritypolicy.autoNonce = true
-
-#--------------------------------------------------------------------
-# COOKIE
-#--------------------------------------------------------------------
-
-# cookie.prefix = ''
-# cookie.expires = 0
-# cookie.path = '/'
-# cookie.domain = ''
-# cookie.secure = false
-# cookie.httponly = false
-# cookie.samesite = 'Lax'
-# cookie.raw = false
-
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
-# encryption.driver = OpenSSL
-# encryption.blockSize = 16
-# encryption.digest = SHA512
-
-#--------------------------------------------------------------------
-# HONEYPOT
-#--------------------------------------------------------------------
-
-# honeypot.hidden = 'true'
-# honeypot.label = 'Fill This Field'
-# honeypot.name = 'honeypot'
-# honeypot.template = '
'
-# honeypot.container = '
{template}
'
-
-#--------------------------------------------------------------------
-# SECURITY
-#--------------------------------------------------------------------
-
-# security.csrfProtection = 'cookie'
-# security.tokenRandomize = false
-# security.tokenName = 'csrf_token_name'
-# security.headerName = 'X-CSRF-TOKEN'
-# security.cookieName = 'csrf_cookie_name'
-# security.expires = 7200
-# security.regenerate = true
-# security.redirect = false
-# security.samesite = 'Lax'
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
-# session.cookieName = 'ci_session'
-# session.expiration = 7200
# session.savePath = null
-# session.matchIP = false
-# session.timeToUpdate = 300
-# session.regenerateDestroy = false
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
# logger.threshold = 4
-
-#--------------------------------------------------------------------
-# CURLRequest
-#--------------------------------------------------------------------
-
-# curlrequest.shareOptions = false
diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml
index 616f6aeae98d..b93122ac193f 100644
--- a/phpdoc.dist.xml
+++ b/phpdoc.dist.xml
@@ -5,12 +5,12 @@
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="https://docs.phpdoc.org/latest/phpdoc.xsd"
>
-
CodeIgniter v4.4 API
+
CodeIgniter v4.5 API
api/cache/
-
+
diff --git a/preload.php b/preload.php
index 63c781c220c5..2fa699388270 100644
--- a/preload.php
+++ b/preload.php
@@ -49,11 +49,12 @@ class preload
*/
private array $paths = [
[
- 'include' => __DIR__ . '/vendor/codeigniter4/framework/system',
+ 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', // Change this path if using manual installation
'exclude' => [
// Not needed if you don't use them.
'/system/Database/OCI8/',
'/system/Database/Postgre/',
+ '/system/Database/SQLite3/',
'/system/Database/SQLSRV/',
// Not needed.
'/system/Database/Seeder.php',
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 8c4f4c696912..53d1b6ce876a 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -85,11 +85,6 @@
-
-
- dom = &$this->domParser]]>
-
-
|parser_callable_string|parser_callable>]]>
diff --git a/psalm_autoload.php b/psalm_autoload.php
index d42636a11fef..b973cc88bc7d 100644
--- a/psalm_autoload.php
+++ b/psalm_autoload.php
@@ -25,6 +25,7 @@
$dirs = [
'tests/_support/Controllers',
+ 'tests/_support/_controller',
'tests/system/Config/fixtures',
];
diff --git a/public/index.php b/public/index.php
index 1cc4710549d5..5ec58a7729c3 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,7 +1,12 @@
systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
-
-// Load environment settings from .env files into $_SERVER and $_ENV
-require_once SYSTEMPATH . 'Config/DotEnv.php';
-(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
-
-// Define ENVIRONMENT
-if (! defined('ENVIRONMENT')) {
- define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
-}
-
-// Load Config Cache
-// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache();
-// $factoriesCache->load('config');
-// ^^^ Uncomment these lines if you want to use Config Caching.
-
-/*
- * ---------------------------------------------------------------
- * GRAB OUR CODEIGNITER INSTANCE
- * ---------------------------------------------------------------
- *
- * The CodeIgniter class contains the core functionality to make
- * the application run, and does all the dirty work to get
- * the pieces all working together.
- */
-
-$app = Config\Services::codeigniter();
-$app->initialize();
-$context = is_cli() ? 'php-cli' : 'web';
-$app->setContext($context);
-
-/*
- *---------------------------------------------------------------
- * LAUNCH THE APPLICATION
- *---------------------------------------------------------------
- * Now that everything is set up, it's time to actually fire
- * up the engines and make this app do its thang.
- */
-
-$app->run();
-
-// Save Config Cache
-// $factoriesCache->save('config');
-// ^^^ Uncomment this line if you want to use Config Caching.
+// LOAD THE FRAMEWORK BOOTSTRAP FILE
+require $paths->systemDirectory . '/Boot.php';
-// Exits the application, setting the exit code for CLI-based applications
-// that might be watching.
-exit(EXIT_SUCCESS);
+exit(CodeIgniter\Boot::bootWeb($paths));
diff --git a/rector.php b/rector.php
index e52be465b4e5..b9d51f872fdc 100644
--- a/rector.php
+++ b/rector.php
@@ -1,5 +1,7 @@
sets([
SetList::DEAD_CODE,
- LevelSetList::UP_TO_PHP_74,
+ LevelSetList::UP_TO_PHP_81,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::PHPUNIT_100,
]);
@@ -90,13 +104,21 @@
__DIR__ . '/system/ThirdParty',
__DIR__ . '/tests/system/Config/fixtures',
__DIR__ . '/tests/system/Filters/fixtures',
- __DIR__ . '/tests/_support',
+ __DIR__ . '/tests/_support/Commands/Foobar.php',
+ __DIR__ . '/tests/_support/View',
+
JsonThrowOnErrorRector::class,
YieldDataProviderRector::class,
+ RemoveUnusedPromotedPropertyRector::class => [
+ // Bug in rector 1.0.0. See https://github.com/rectorphp/rector-src/pull/5573
+ __DIR__ . '/tests/_support/Entity/CustomUser.php',
+ ],
+
RemoveUnusedPrivateMethodRector::class => [
// private method called via getPrivateMethodInvoker
__DIR__ . '/tests/system/Test/ReflectionHelperTest.php',
+ __DIR__ . '/tests/_support/Test/TestForReflectionHelper.php',
],
RemoveUnusedConstructorParamRector::class => [
@@ -106,6 +128,7 @@
__DIR__ . '/system/Router/AutoRouterImproved.php',
// @TODO remove if deprecated $config is removed
__DIR__ . '/system/HTTP/Request.php',
+ __DIR__ . '/system/HTTP/Response.php',
],
// check on constant compare
@@ -113,21 +136,86 @@
__DIR__ . '/system/Autoloader/Autoloader.php',
],
- // session handlers have the gc() method with underscored parameter `$max_lifetime`
UnderscoreToCamelCaseVariableNameRector::class => [
+ // session handlers have the gc() method with underscored parameter `$max_lifetime`
__DIR__ . '/system/Session/Handlers',
+ __DIR__ . '/tests/_support/Entity/CustomUser.php',
+ ],
+
+ DeclareStrictTypesRector::class => [
+ __DIR__ . '/app',
+ __DIR__ . '/system/CodeIgniter.php',
+ __DIR__ . '/system/Config/BaseConfig.php',
+ __DIR__ . '/system/Commands/Generators/Views',
+ __DIR__ . '/system/Pager/Views',
+ __DIR__ . '/system/Test/ControllerTestTrait.php',
+ __DIR__ . '/system/Validation/Views',
+ __DIR__ . '/system/View/Parser.php',
+ __DIR__ . '/tests/system/Debug/ExceptionsTest.php',
+ __DIR__ . '/tests/system/View/Views',
],
// use mt_rand instead of random_int on purpose on non-cryptographically random
RandomFunctionRector::class,
SimplifyRegexPatternRector::class,
+
+ // PHP 8.0 features but cause breaking changes
+ ClassPropertyAssignToConstructorPromotionRector::class => [
+ __DIR__ . '/system/Database/BaseResult.php',
+ __DIR__ . '/system/Database/RawSql.php',
+ __DIR__ . '/system/Debug/BaseExceptionHandler.php',
+ __DIR__ . '/system/Filters/Filters.php',
+ __DIR__ . '/system/HTTP/CURLRequest.php',
+ __DIR__ . '/system/HTTP/DownloadResponse.php',
+ __DIR__ . '/system/HTTP/IncomingRequest.php',
+ __DIR__ . '/system/Security/Security.php',
+ __DIR__ . '/system/Session/Session.php',
+ ],
+ MixedTypeRector::class,
+
+ // PHP 8.1 features but cause breaking changes
+ FinalizePublicClassConstantRector::class => [
+ __DIR__ . '/system/Cache/Handlers/BaseHandler.php',
+ __DIR__ . '/system/Cache/Handlers/FileHandler.php',
+ __DIR__ . '/system/CodeIgniter.php',
+ __DIR__ . '/system/Events/Events.php',
+ __DIR__ . '/system/Log/Handlers/ChromeLoggerHandler.php',
+ __DIR__ . '/system/Log/Handlers/ErrorlogHandler.php',
+ __DIR__ . '/system/Security/Security.php',
+ ],
+ ReturnNeverTypeRector::class => [
+ __DIR__ . '/system/Cache/Handlers/BaseHandler.php',
+ __DIR__ . '/system/Cache/Handlers/MemcachedHandler.php',
+ __DIR__ . '/system/Cache/Handlers/WincacheHandler.php',
+ __DIR__ . '/system/CodeIgniter.php',
+ __DIR__ . '/system/Database/MySQLi/Utils.php',
+ __DIR__ . '/system/Database/OCI8/Utils.php',
+ __DIR__ . '/system/Database/Postgre/Utils.php',
+ __DIR__ . '/system/Database/SQLSRV/Utils.php',
+ __DIR__ . '/system/Database/SQLite3/Utils.php',
+ __DIR__ . '/system/HTTP/DownloadResponse.php',
+ __DIR__ . '/system/HTTP/SiteURI.php',
+ __DIR__ . '/system/Helpers/kint_helper.php',
+ __DIR__ . '/tests/_support/Autoloader/FatalLocator.php',
+ ],
+
+ // Unnecessary (string) is inserted
+ NullToStrictStringFuncCallArgRector::class,
+
+ // PHPUnit 10 (requires PHP 8.1) features
+ DataProviderAnnotationToAttributeRector::class,
+ DependsAnnotationWithValueToAttributeRector::class,
+ AnnotationWithValueToAttributeRector::class,
+ AnnotationToAttributeRector::class,
+ CoversAnnotationWithValueToAttributeRector::class,
]);
// auto import fully qualified class names
$rectorConfig->importNames();
$rectorConfig->removeUnusedImports();
+ $rectorConfig->rule(DeclareStrictTypesRector::class);
$rectorConfig->rule(UnderscoreToCamelCaseVariableNameRector::class);
$rectorConfig->rule(SimplifyUselessVariableRector::class);
$rectorConfig->rule(RemoveAlwaysElseRector::class);
diff --git a/spark b/spark
index 9daa44034473..a56fbc1bd7b6 100755
--- a/spark
+++ b/spark
@@ -12,13 +12,16 @@
/*
* --------------------------------------------------------------------
- * CodeIgniter command-line tools
+ * CODEIGNITER COMMAND-LINE TOOLS
* --------------------------------------------------------------------
* The main entry point into the CLI system and allows you to run
* commands and perform maintenance on your application.
- *
- * Because CodeIgniter can handle CLI requests as just another web request
- * this class mainly acts as a passthru to the framework itself.
+ */
+
+/*
+ *---------------------------------------------------------------
+ * CHECK SERVER API
+ *---------------------------------------------------------------
*/
// Refuse to run when called from php-cgi
@@ -26,8 +29,13 @@ if (strpos(PHP_SAPI, 'cgi') === 0) {
exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n");
}
-// Check PHP version.
-$minPhpVersion = '7.4'; // If you update this, don't forget to update `public/index.php`.
+/*
+ *---------------------------------------------------------------
+ * CHECK PHP VERSION
+ *---------------------------------------------------------------
+ */
+
+$minPhpVersion = '8.1'; // If you update this, don't forget to update `public/index.php`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
$message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
@@ -42,12 +50,11 @@ if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
-/**
- * @var bool
- *
- * @deprecated No longer in use. `CodeIgniter` has `$context` property.
+/*
+ *---------------------------------------------------------------
+ * SET THE CURRENT DIRECTORY
+ *---------------------------------------------------------------
*/
-define('SPARKED', true);
// Path to the front controller
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
@@ -64,41 +71,14 @@ chdir(FCPATH);
* and fires up an environment-specific bootstrapping.
*/
-// Load our paths config file
+// LOAD OUR PATHS CONFIG FILE
// This is the line that might need to be changed, depending on your folder structure.
require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder
$paths = new Config\Paths();
-// Location of the framework bootstrap file.
-require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
-
-// Load environment settings from .env files into $_SERVER and $_ENV
-require_once SYSTEMPATH . 'Config/DotEnv.php';
-(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
-
-// Define ENVIRONMENT
-if (! defined('ENVIRONMENT')) {
- define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
-}
-
-// Grab our CodeIgniter
-$app = Config\Services::codeigniter();
-$app->initialize();
-
-// Grab our Console
-$console = new CodeIgniter\CLI\Console();
-
-// Show basic information before we do anything else.
-if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) {
- unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore
- $suppress = true;
-}
-
-$console->showHeader($suppress);
-
-// fire off the command in the main framework.
-$exit = $console->run();
+// LOAD THE FRAMEWORK BOOTSTRAP FILE
+require $paths->systemDirectory . '/Boot.php';
-exit(is_int($exit) ? $exit : EXIT_SUCCESS);
+exit(CodeIgniter\Boot::bootSpark($paths));
diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php
index 83a14528f6c3..01ce42860b32 100644
--- a/system/API/ResponseTrait.php
+++ b/system/API/ResponseTrait.php
@@ -1,5 +1,7 @@
response->getHeaderLine('Content-Type');
- $contentType = str_replace('application/json', 'text/html', $contentType);
- $contentType = str_replace('application/', 'text/', $contentType);
- $this->response->setContentType($contentType);
- $this->format = 'html';
-
- return $data;
- }
+ $format = service('format');
- $format = Services::format();
- $mime = "application/{$this->format}";
+ $mime = ($this->format === null) ? $format->getConfig()->supportedResponseFormats[0]
+ : "application/{$this->format}";
// Determine correct response type through content negotiation if not explicitly declared
if (
@@ -336,6 +331,23 @@ protected function format($data = null)
$this->formatter = $format->getFormatter($mime);
}
+ $asHtml = $this->stringAsHtml ?? false;
+
+ // Returns as HTML.
+ if (
+ ($mime === 'application/json' && $asHtml && is_string($data))
+ || ($mime !== 'application/json' && is_string($data))
+ ) {
+ // The content type should be text/... and not application/...
+ $contentType = $this->response->getHeaderLine('Content-Type');
+ $contentType = str_replace('application/json', 'text/html', $contentType);
+ $contentType = str_replace('application/', 'text/', $contentType);
+ $this->response->setContentType($contentType);
+ $this->format = 'html';
+
+ return $data;
+ }
+
if ($mime !== 'application/json') {
// Recursively convert objects into associative arrays
// Conversion not required for JSONFormatter
@@ -348,11 +360,14 @@ protected function format($data = null)
/**
* Sets the format the response should be in.
*
+ * @param string|null $format Response format
+ * @phpstan-param 'json'|'xml' $format
+ *
* @return $this
*/
protected function setResponseFormat(?string $format = null)
{
- $this->format = strtolower($format);
+ $this->format = ($format === null) ? null : strtolower($format);
return $this;
}
diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php
index 088c850efa58..78d31eae0d7f 100644
--- a/system/Autoloader/Autoloader.php
+++ b/system/Autoloader/Autoloader.php
@@ -1,5 +1,7 @@
+ * @var array
*/
protected $classmap = [];
@@ -139,8 +146,6 @@ private function loadComposerAutoloader(Modules $modules): void
/** @var ClassLoader $composer */
$composer = include COMPOSER_PATH;
- $this->loadComposerClassmap($composer);
-
// Should we load through Composer's namespaces, also?
if ($modules->discoverInComposer) {
// @phpstan-ignore-next-line
@@ -157,11 +162,11 @@ private function loadComposerAutoloader(Modules $modules): void
*/
public function register()
{
- // Prepend the PSR4 autoloader for maximum performance.
- spl_autoload_register([$this, 'loadClass'], true, true);
+ // Register classmap loader for the files in our class map.
+ spl_autoload_register($this->loadClassmap(...), true);
- // Now prepend another loader for the files in our class map.
- spl_autoload_register([$this, 'loadClassmap'], true, true);
+ // Register the PSR-4 autoloader.
+ spl_autoload_register($this->loadClass(...), true);
// Load our non-class files
foreach ($this->files as $file) {
@@ -176,8 +181,8 @@ public function register()
*/
public function unregister(): void
{
- spl_autoload_unregister([$this, 'loadClass']);
- spl_autoload_unregister([$this, 'loadClassmap']);
+ spl_autoload_unregister($this->loadClass(...));
+ spl_autoload_unregister($this->loadClassmap(...));
}
/**
@@ -215,7 +220,8 @@ public function addNamespace($namespace, ?string $path = null)
*
* If a prefix param is set, returns only paths to the given prefix.
*
- * @return array
+ * @return array>|list
+ * @phpstan-return ($prefix is null ? array> : list)
*/
public function getNamespace(?string $prefix = null)
{
@@ -275,12 +281,12 @@ public function loadClass(string $class): void
*/
protected function loadInNamespace(string $class)
{
- if (strpos($class, '\\') === false) {
+ if (! str_contains($class, '\\')) {
return false;
}
foreach ($this->prefixes as $namespace => $directories) {
- if (strpos($class, $namespace) === 0) {
+ if (str_starts_with($class, $namespace)) {
$relativeClassPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($namespace)));
foreach ($directories as $directory) {
@@ -346,7 +352,7 @@ public function sanitizeFilename(string $filename): string
);
}
if ($result === false) {
- $message = PHP_VERSION_ID >= 80000 ? preg_last_error_msg() : 'Regex error. error code: ' . preg_last_error();
+ $message = preg_last_error_msg();
throw new RuntimeException($message . '. filename: "' . $filename . '"');
}
@@ -365,9 +371,13 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa
{
$namespacePaths = $composer->getPrefixesPsr4();
- // Get rid of CodeIgniter so we don't have duplicates
- if (isset($namespacePaths['CodeIgniter\\'])) {
- unset($namespacePaths['CodeIgniter\\']);
+ // Get rid of duplicated namespaces.
+ $duplicatedNamespaces = ['CodeIgniter', APP_NAMESPACE, 'Config'];
+
+ foreach ($duplicatedNamespaces as $ns) {
+ if (isset($namespacePaths[$ns . '\\'])) {
+ unset($namespacePaths[$ns . '\\']);
+ }
}
if (! method_exists(InstalledVersions::class, 'getAllRawData')) {
@@ -415,7 +425,7 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa
foreach ($srcPaths as $path) {
foreach ($installPaths as $installPath) {
- if ($installPath === substr($path, 0, strlen($installPath))) {
+ if (str_starts_with($path, $installPath)) {
$add = true;
break 2;
}
@@ -431,13 +441,6 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa
$this->addNamespace($newPaths);
}
- private function loadComposerClassmap(ClassLoader $composer): void
- {
- $classes = $composer->getClassMap();
-
- $this->classmap = array_merge($this->classmap, $classes);
- }
-
/**
* Locates autoload information from Composer, if available.
*
@@ -483,4 +486,76 @@ public function loadHelpers(): void
{
helper($this->helpers);
}
+
+ /**
+ * Initializes Kint
+ */
+ public function initializeKint(bool $debug = false): void
+ {
+ if ($debug) {
+ $this->autoloadKint();
+ $this->configureKint();
+ } elseif (class_exists(Kint::class)) {
+ // In case that Kint is already loaded via Composer.
+ Kint::$enabled_mode = false;
+ }
+
+ helper('kint');
+ }
+
+ private function autoloadKint(): void
+ {
+ // If we have KINT_DIR it means it's already loaded via composer
+ if (! defined('KINT_DIR')) {
+ spl_autoload_register(function ($class) {
+ $class = explode('\\', $class);
+
+ if (array_shift($class) !== 'Kint') {
+ return;
+ }
+
+ $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
+
+ if (is_file($file)) {
+ require_once $file;
+ }
+ });
+
+ require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
+ }
+ }
+
+ private function configureKint(): void
+ {
+ $config = new KintConfig();
+
+ Kint::$depth_limit = $config->maxDepth;
+ Kint::$display_called_from = $config->displayCalledFrom;
+ Kint::$expanded = $config->expanded;
+
+ if (isset($config->plugins) && is_array($config->plugins)) {
+ Kint::$plugins = $config->plugins;
+ }
+
+ $csp = Services::csp();
+ if ($csp->enabled()) {
+ RichRenderer::$js_nonce = $csp->getScriptNonce();
+ RichRenderer::$css_nonce = $csp->getStyleNonce();
+ }
+
+ RichRenderer::$theme = $config->richTheme;
+ RichRenderer::$folder = $config->richFolder;
+ RichRenderer::$sort = $config->richSort;
+ if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
+ RichRenderer::$value_plugins = $config->richObjectPlugins;
+ }
+ if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
+ RichRenderer::$tab_plugins = $config->richTabPlugins;
+ }
+
+ CliRenderer::$cli_colors = $config->cliColors;
+ CliRenderer::$force_utf8 = $config->cliForceUTF8;
+ CliRenderer::$detect_width = $config->cliDetectWidth;
+ CliRenderer::$min_terminal_width = $config->cliMinWidth;
+ }
}
diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php
index 6f1c547d2358..97da0fa275eb 100644
--- a/system/Autoloader/FileLocator.php
+++ b/system/Autoloader/FileLocator.php
@@ -1,5 +1,7 @@
ensureExt($file, $ext);
// Clears the folder name if it is at the beginning of the filename
- if ($folder !== null && strpos($file, $folder) === 0) {
+ if ($folder !== null && str_starts_with($file, $folder)) {
$file = substr($file, strlen($folder . '/'));
}
// Is not namespaced? Try the application folder.
- if (strpos($file, '\\') === false) {
+ if (! str_contains($file, '\\')) {
return $this->legacyLocate($file, $folder);
}
@@ -101,7 +103,7 @@ public function locateFile(string $file, ?string $folder = null, string $ext = '
// If we have a folder name, then the calling function
// expects this file to be within that folder, like 'Views',
// or 'libraries'.
- if ($folder !== null && strpos($path . $filename, '/' . $folder . '/') === false) {
+ if ($folder !== null && ! str_contains($path . $filename, '/' . $folder . '/')) {
$path .= trim($folder, '/') . '/';
}
@@ -173,6 +175,8 @@ public function getClassname(string $file): string
* 'app/Modules/foo/Config/Routes.php',
* 'app/Modules/bar/Config/Routes.php',
* ]
+ *
+ * @return list
*/
public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array
{
@@ -188,7 +192,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp =
if ($prioritizeApp) {
$foundPaths[] = $fullPath;
- } elseif (strpos($fullPath, APPPATH) === 0) {
+ } elseif (str_starts_with($fullPath, APPPATH)) {
$appPaths[] = $fullPath;
} else {
$foundPaths[] = $fullPath;
@@ -201,7 +205,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp =
}
// Remove any duplicates
- return array_unique($foundPaths);
+ return array_values(array_unique($foundPaths));
}
/**
@@ -212,7 +216,7 @@ protected function ensureExt(string $path, string $ext): string
if ($ext !== '') {
$ext = '.' . $ext;
- if (substr($path, -strlen($ext)) !== $ext) {
+ if (! str_ends_with($path, $ext)) {
$path .= $ext;
}
}
@@ -235,7 +239,7 @@ protected function getNamespaces()
foreach ($this->autoloader->getNamespace() as $prefix => $paths) {
foreach ($paths as $path) {
if ($prefix === 'CodeIgniter') {
- $system = [
+ $system[] = [
'prefix' => $prefix,
'path' => rtrim($path, '\\/') . DIRECTORY_SEPARATOR,
];
@@ -250,9 +254,7 @@ protected function getNamespaces()
}
}
- $namespaces[] = $system;
-
- return $namespaces;
+ return array_merge($namespaces, $system);
}
/**
@@ -277,12 +279,15 @@ public function findQualifiedNameFromPath(string $path)
}
if (mb_strpos($path, $namespace['path']) === 0) {
- $className = '\\' . $namespace['prefix'] . '\\' .
- ltrim(str_replace(
+ $className = $namespace['prefix'] . '\\' .
+ ltrim(
+ str_replace(
'/',
'\\',
mb_substr($path, mb_strlen($namespace['path']))
- ), '\\');
+ ),
+ '\\'
+ );
// Remove the file extension (.php)
$className = mb_substr($className, 0, -4);
diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php
new file mode 100644
index 000000000000..adf453308de3
--- /dev/null
+++ b/system/Autoloader/FileLocatorCached.php
@@ -0,0 +1,172 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Autoloader;
+
+use CodeIgniter\Cache\CacheInterface;
+use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler;
+
+/**
+ * FileLocator with Cache
+ *
+ * @see \CodeIgniter\Autoloader\FileLocatorCachedTest
+ */
+final class FileLocatorCached implements FileLocatorInterface
+{
+ /**
+ * @var CacheInterface|FileVarExportHandler
+ */
+ private $cacheHandler;
+
+ /**
+ * Cache data
+ *
+ * [method => data]
+ * E.g.,
+ * [
+ * 'search' => [$path => $foundPaths],
+ * ]
+ */
+ private array $cache = [];
+
+ /**
+ * Is the cache updated?
+ */
+ private bool $cacheUpdated = false;
+
+ private string $cacheKey = 'FileLocatorCache';
+
+ /**
+ * @param CacheInterface|FileVarExportHandler|null $cache
+ */
+ public function __construct(private readonly FileLocator $locator, $cache = null)
+ {
+ $this->cacheHandler = $cache ?? new FileVarExportHandler();
+
+ $this->loadCache();
+ }
+
+ private function loadCache(): void
+ {
+ $data = $this->cacheHandler->get($this->cacheKey);
+
+ if (is_array($data)) {
+ $this->cache = $data;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->saveCache();
+ }
+
+ private function saveCache(): void
+ {
+ if ($this->cacheUpdated) {
+ $this->cacheHandler->save($this->cacheKey, $this->cache, 3600 * 24);
+ }
+ }
+
+ /**
+ * Delete cache data
+ */
+ public function deleteCache(): void
+ {
+ $this->cacheUpdated = false;
+ $this->cacheHandler->delete($this->cacheKey);
+ }
+
+ public function findQualifiedNameFromPath(string $path): false|string
+ {
+ if (isset($this->cache['findQualifiedNameFromPath'][$path])) {
+ return $this->cache['findQualifiedNameFromPath'][$path];
+ }
+
+ $classname = $this->locator->findQualifiedNameFromPath($path);
+
+ $this->cache['findQualifiedNameFromPath'][$path] = $classname;
+ $this->cacheUpdated = true;
+
+ return $classname;
+ }
+
+ public function getClassname(string $file): string
+ {
+ if (isset($this->cache['getClassname'][$file])) {
+ return $this->cache['getClassname'][$file];
+ }
+
+ $classname = $this->locator->getClassname($file);
+
+ $this->cache['getClassname'][$file] = $classname;
+ $this->cacheUpdated = true;
+
+ return $classname;
+ }
+
+ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array
+ {
+ if (isset($this->cache['search'][$path][$ext][$prioritizeApp])) {
+ return $this->cache['search'][$path][$ext][$prioritizeApp];
+ }
+
+ $foundPaths = $this->locator->search($path, $ext, $prioritizeApp);
+
+ $this->cache['search'][$path][$ext][$prioritizeApp] = $foundPaths;
+ $this->cacheUpdated = true;
+
+ return $foundPaths;
+ }
+
+ public function listFiles(string $path): array
+ {
+ if (isset($this->cache['listFiles'][$path])) {
+ return $this->cache['listFiles'][$path];
+ }
+
+ $files = $this->locator->listFiles($path);
+
+ $this->cache['listFiles'][$path] = $files;
+ $this->cacheUpdated = true;
+
+ return $files;
+ }
+
+ public function listNamespaceFiles(string $prefix, string $path): array
+ {
+ if (isset($this->cache['listNamespaceFiles'][$prefix][$path])) {
+ return $this->cache['listNamespaceFiles'][$prefix][$path];
+ }
+
+ $files = $this->locator->listNamespaceFiles($prefix, $path);
+
+ $this->cache['listNamespaceFiles'][$prefix][$path] = $files;
+ $this->cacheUpdated = true;
+
+ return $files;
+ }
+
+ public function locateFile(string $file, ?string $folder = null, string $ext = 'php'): false|string
+ {
+ if (isset($this->cache['locateFile'][$file][$folder][$ext])) {
+ return $this->cache['locateFile'][$file][$folder][$ext];
+ }
+
+ $files = $this->locator->locateFile($file, $folder, $ext);
+
+ $this->cache['locateFile'][$file][$folder][$ext] = $files;
+ $this->cacheUpdated = true;
+
+ return $files;
+ }
+}
diff --git a/system/Autoloader/FileLocatorInterface.php b/system/Autoloader/FileLocatorInterface.php
new file mode 100644
index 000000000000..3f7355a8f09d
--- /dev/null
+++ b/system/Autoloader/FileLocatorInterface.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Autoloader;
+
+/**
+ * Allows loading non-class files in a namespaced manner.
+ * Works with Helpers, Views, etc.
+ */
+interface FileLocatorInterface
+{
+ /**
+ * 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 relative file path or namespaced file to
+ * locate. If not namespaced, search in the app
+ * folder.
+ * @param non-empty-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.
+ */
+ public function locateFile(string $file, ?string $folder = null, string $ext = 'php');
+
+ /**
+ * Examines a file and returns the fully qualified class name.
+ */
+ public function getClassname(string $file): string;
+
+ /**
+ * Searches through all of the defined namespaces looking for a file.
+ * Returns an array of all found locations for the defined file.
+ *
+ * Example:
+ *
+ * $locator->search('Config/Routes.php');
+ * // Assuming PSR4 namespaces include foo and bar, might return:
+ * [
+ * 'app/Modules/foo/Config/Routes.php',
+ * 'app/Modules/bar/Config/Routes.php',
+ * ]
+ */
+ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array;
+
+ /**
+ * Find the qualified name of a file according to
+ * the namespace of the first matched namespace path.
+ *
+ * @return false|string The qualified name or false if the path is not found
+ */
+ public function findQualifiedNameFromPath(string $path);
+
+ /**
+ * Scans the defined namespaces, returning a list of all files
+ * that are contained within the subpath specified by $path.
+ *
+ * @return list List of file paths
+ */
+ public function listFiles(string $path): array;
+
+ /**
+ * Scans the provided namespace, returning a list of all files
+ * that are contained within the sub path specified by $path.
+ *
+ * @return list List of file paths
+ */
+ public function listNamespaceFiles(string $prefix, string $path): array;
+}
diff --git a/system/BaseModel.php b/system/BaseModel.php
index cd2bdd6beb22..5d08ec59e04c 100644
--- a/system/BaseModel.php
+++ b/system/BaseModel.php
@@ -1,5 +1,7 @@
[column => type]
+ */
+ protected array $casts = [];
+
+ /**
+ * Custom convert handlers.
+ *
+ * @var array [type => classname]
+ */
+ protected array $castHandlers = [];
+
+ protected ?DataConverter $converter = null;
+
/**
* If this model should use "softDeletes" and
* simply set a date when rows are deleted, or
@@ -92,7 +128,7 @@ abstract class BaseModel
*
* @var bool
*/
- protected $useSoftDeletes = false;
+ protected $protectFields = true;
/**
* An array of field names that are allowed
@@ -134,6 +170,15 @@ abstract class BaseModel
*/
protected $updatedField = 'updated_at';
+ /**
+ * If this model should use "softDeletes" and
+ * simply set a date when rows are deleted, or
+ * do hard deletes.
+ *
+ * @var bool
+ */
+ protected $useSoftDeletes = false;
+
/**
* Used by withDeleted to override the
* model's softDelete setting.
@@ -150,27 +195,14 @@ abstract class BaseModel
protected $deletedField = 'deleted_at';
/**
- * Used by asArray and asObject to provide
- * temporary overrides of model default.
- *
- * @var string
- */
- protected $tempReturnType;
-
- /**
- * Whether we should limit fields in inserts
- * and updates to those available in $allowedFields or not.
- *
- * @var bool
+ * Whether to allow inserting empty data.
*/
- protected $protectFields = true;
+ protected bool $allowEmptyInserts = false;
/**
- * Database Connection
- *
- * @var BaseConnection
+ * Whether to update Entity's only changed data.
*/
- protected $db;
+ protected bool $updateOnlyChanged = true;
/**
* Rules used to validate data in insert(), update(), and save() methods.
@@ -211,7 +243,7 @@ abstract class BaseModel
/**
* Our validator instance.
*
- * @var ValidationInterface
+ * @var ValidationInterface|null
*/
protected $validation;
@@ -327,24 +359,38 @@ abstract class BaseModel
*/
protected $afterDelete = [];
- /**
- * Whether to allow inserting empty data.
- */
- protected bool $allowEmptyInserts = false;
-
public function __construct(?ValidationInterface $validation = null)
{
$this->tempReturnType = $this->returnType;
$this->tempUseSoftDeletes = $this->useSoftDeletes;
$this->tempAllowCallbacks = $this->allowCallbacks;
- /**
- * @var ValidationInterface|null $validation
- */
- $validation ??= Services::validation(null, false);
$this->validation = $validation;
$this->initialize();
+ $this->createDataConverter();
+ }
+
+ /**
+ * Creates DataConverter instance.
+ */
+ protected function createDataConverter(): void
+ {
+ if ($this->useCasts()) {
+ $this->converter = new DataConverter(
+ $this->casts,
+ $this->castHandlers,
+ $this->db
+ );
+ }
+ }
+
+ /**
+ * Are casts used?
+ */
+ protected function useCasts(): bool
+ {
+ return $this->casts !== [];
}
/**
@@ -384,12 +430,12 @@ abstract protected function doFindColumn(string $columnName);
* Fetches all results, while optionally limiting them.
* This method works only with dbCalls.
*
- * @param int $limit Limit
- * @param int $offset Offset
+ * @param int|null $limit Limit
+ * @param int $offset Offset
*
* @return array
*/
- abstract protected function doFindAll(int $limit = 0, int $offset = 0);
+ abstract protected function doFindAll(?int $limit = null, int $offset = 0);
/**
* Returns the first row of the result set.
@@ -499,17 +545,6 @@ abstract protected function doReplace(?array $row = null, bool $returnSQL = fals
*/
abstract protected function doErrors();
- /**
- * Returns the id value for the data array or object.
- *
- * @param array|object $data Data
- *
- * @return array|int|string|null
- *
- * @deprecated Add an override on getIdValue() instead. Will be removed in version 5.0.
- */
- abstract protected function idValue($data);
-
/**
* Public getter to return the id value using the idValue() method.
* For example with SQL this will return $data->$this->primaryKey.
@@ -518,13 +553,8 @@ abstract protected function idValue($data);
* @phpstan-param row_array|object $row
*
* @return array|int|string|null
- *
- * @todo: Make abstract in version 5.0
*/
- public function getIdValue($row)
- {
- return $this->idValue($row);
- }
+ abstract public function getIdValue($row);
/**
* Override countAllResults to account for soft deleted accounts.
@@ -604,7 +634,7 @@ public function find($id = null)
*/
public function findColumn(string $columnName)
{
- if (strpos($columnName, ',') !== false) {
+ if (str_contains($columnName, ',')) {
throw DataException::forFindColumnHaveMultipleColumns();
}
@@ -621,8 +651,13 @@ public function findColumn(string $columnName)
*
* @return array
*/
- public function findAll(int $limit = 0, int $offset = 0)
+ public function findAll(?int $limit = null, int $offset = 0)
{
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll) {
+ $limit ??= 0;
+ }
+
if ($this->tempAllowCallbacks) {
// Call the before event and check for a return
$eventData = $this->trigger('beforeFind', [
@@ -1208,6 +1243,10 @@ public function replace(?array $row = null, bool $returnSQL = false)
*/
public function errors(bool $forceDB = false)
{
+ if ($this->validation === null) {
+ return $this->doErrors();
+ }
+
// Do we have validation errors?
if (! $forceDB && ! $this->skipValidation && ($errors = $this->validation->getErrors())) {
return $errors;
@@ -1231,7 +1270,7 @@ public function errors(bool $forceDB = false)
public function paginate(?int $perPage = null, string $group = 'default', ?int $page = null, int $segment = 0)
{
// Since multiple models may use the Pager, the Pager must be shared.
- $pager = Services::pager();
+ $pager = service('pager');
if ($segment !== 0) {
$pager->setSegment($segment, $group);
@@ -1358,19 +1397,12 @@ protected function setDate(?int $userData = null)
*/
protected function intToDate(int $value)
{
- switch ($this->dateFormat) {
- case 'int':
- return $value;
-
- case 'datetime':
- return date('Y-m-d H:i:s', $value);
-
- case 'date':
- return date('Y-m-d', $value);
-
- default:
- throw ModelException::forNoDateFormat(static::class);
- }
+ return match ($this->dateFormat) {
+ 'int' => $value,
+ 'datetime' => date($this->db->dateFormat['datetime'], $value),
+ 'date' => date($this->db->dateFormat['date'], $value),
+ default => throw ModelException::forNoDateFormat(static::class),
+ };
}
/**
@@ -1387,19 +1419,12 @@ protected function intToDate(int $value)
*/
protected function timeToDate(Time $value)
{
- switch ($this->dateFormat) {
- case 'datetime':
- return $value->format('Y-m-d H:i:s');
-
- case 'date':
- return $value->format('Y-m-d');
-
- case 'int':
- return $value->getTimestamp();
-
- default:
- return (string) $value;
- }
+ return match ($this->dateFormat) {
+ 'datetime' => $value->format($this->db->dateFormat['datetime']),
+ 'date' => $value->format($this->db->dateFormat['date']),
+ 'int' => $value->getTimestamp(),
+ default => (string) $value,
+ };
}
/**
@@ -1478,6 +1503,8 @@ public function setValidationRule(string $field, $fieldRules)
// ValidationRules can be either a string, which is the group name,
// or an array of rules.
if (is_string($rules)) {
+ $this->ensureValidation();
+
[$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
$this->validationRules = $rules;
@@ -1513,14 +1540,22 @@ public function cleanRules(bool $choice = false)
*/
public function validate($row): bool
{
+ if ($this->skipValidation) {
+ return true;
+ }
+
$rules = $this->getValidationRules();
+ if ($rules === []) {
+ return true;
+ }
+
// Validation requires array, so cast away.
if (is_object($row)) {
$row = (array) $row;
}
- if ($this->skipValidation || $rules === [] || $row === []) {
+ if ($row === []) {
return true;
}
@@ -1532,6 +1567,8 @@ public function validate($row): bool
return true;
}
+ $this->ensureValidation();
+
$this->validation->reset()->setRules($rules, $this->validationMessages);
return $this->validation->run($row, null, $this->DBGroup);
@@ -1550,6 +1587,8 @@ public function getValidationRules(array $options = []): array
// ValidationRules can be either a string, which is the group name,
// or an array of rules.
if (is_string($rules)) {
+ $this->ensureValidation();
+
[$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
$this->validationMessages += $customErrors;
@@ -1564,6 +1603,13 @@ public function getValidationRules(array $options = []): array
return $rules;
}
+ protected function ensureValidation(): void
+ {
+ if ($this->validation === null) {
+ $this->validation = Services::validation(null, false);
+ }
+ }
+
/**
* Returns the model's validation messages, so they
* can be used elsewhere, if needed.
@@ -1670,7 +1716,7 @@ public function asArray()
* class vars with the same name as the collection columns,
* or at least allows them to be created.
*
- * @param string $class Class Name
+ * @param 'object'|class-string $class Class Name
*
* @return $this
*/
@@ -1733,7 +1779,7 @@ protected function timeToString(array $properties): array
* @param bool $onlyChanged Only Changed Property
* @param bool $recursive If true, inner entities will be casted as array as well
*
- * @return array
+ * @return array Array with raw values.
*
* @throws ReflectionException
*/
@@ -1770,6 +1816,9 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec
* @throws DataException
* @throws InvalidArgumentException
* @throws ReflectionException
+ *
+ * @used-by insert()
+ * @used-by update()
*/
protected function transformDataToArray($row, string $type): array
{
@@ -1781,14 +1830,31 @@ protected function transformDataToArray($row, string $type): array
throw DataException::forEmptyDataset($type);
}
+ // If it validates with entire rules, all fields are needed.
+ if ($this->skipValidation === false && $this->cleanValidationRules === false) {
+ $onlyChanged = false;
+ } else {
+ $onlyChanged = ($type === 'update' && $this->updateOnlyChanged);
+ }
+
+ if ($this->useCasts()) {
+ if (is_array($row)) {
+ $row = $this->converter->toDataSource($row);
+ } elseif ($row instanceof stdClass) {
+ $row = (array) $row;
+ $row = $this->converter->toDataSource($row);
+ } elseif ($row instanceof Entity) {
+ $row = $this->converter->extract($row, $onlyChanged);
+ // Convert any Time instances to appropriate $dateFormat
+ $row = $this->timeToString($row);
+ } elseif (is_object($row)) {
+ $row = $this->converter->extract($row, $onlyChanged);
+ }
+ }
// If $row is using a custom class with public or protected
// properties representing the collection elements, we need to grab
// them as an array.
- if (is_object($row) && ! $row instanceof stdClass) {
- // If it validates with entire rules, all fields are needed.
- $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false)
- ? false : ($type === 'update');
-
+ elseif (is_object($row) && ! $row instanceof stdClass) {
$row = $this->objectToArray($row, $onlyChanged, true);
}
@@ -1855,65 +1921,31 @@ public function __call(string $name, array $params)
}
/**
- * Replace any placeholders within the rules with the values that
- * match the 'key' of any properties being set. For example, if
- * we had the following $data array:
- *
- * [ 'id' => 13 ]
- *
- * and the following rule:
- *
- * 'required|is_unique[users,email,id,{id}]'
- *
- * The value of {id} would be replaced with the actual id in the form data:
- *
- * 'required|is_unique[users,email,id,13]'
- *
- * @param array $rules Validation rules
- * @param array $data Data
- *
- * @codeCoverageIgnore
- *
- * @deprecated use fillPlaceholders($rules, $data) from Validation instead
+ * Sets $allowEmptyInserts.
*/
- protected function fillPlaceholders(array $rules, array $data): array
+ public function allowEmptyInserts(bool $value = true): self
{
- $replacements = [];
-
- foreach ($data as $key => $value) {
- $replacements['{' . $key . '}'] = $value;
- }
-
- if ($replacements !== []) {
- foreach ($rules as &$rule) {
- if (is_array($rule)) {
- foreach ($rule as &$row) {
- // Should only be an `errors` array
- // which doesn't take placeholders.
- if (is_array($row)) {
- continue;
- }
-
- $row = strtr($row, $replacements);
- }
-
- continue;
- }
-
- $rule = strtr($rule, $replacements);
- }
- }
+ $this->allowEmptyInserts = $value;
- return $rules;
+ return $this;
}
/**
- * Sets $allowEmptyInserts.
+ * Converts database data array to return type value.
+ *
+ * @param array $row Raw data from database
+ * @param 'array'|'object'|class-string $returnType
*/
- public function allowEmptyInserts(bool $value = true): self
+ protected function convertToReturnType(array $row, string $returnType): array|object
{
- $this->allowEmptyInserts = $value;
+ if ($returnType === 'array') {
+ return $this->converter->fromDataSource($row);
+ }
- return $this;
+ if ($returnType === 'object') {
+ return (object) $this->converter->fromDataSource($row);
+ }
+
+ return $this->converter->reconstruct($returnType, $row);
}
}
diff --git a/system/Boot.php b/system/Boot.php
new file mode 100644
index 000000000000..8a769bbb6866
--- /dev/null
+++ b/system/Boot.php
@@ -0,0 +1,342 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter;
+
+use CodeIgniter\Cache\FactoriesCache;
+use CodeIgniter\CLI\Console;
+use CodeIgniter\Config\DotEnv;
+use Config\Autoload;
+use Config\Modules;
+use Config\Optimize;
+use Config\Paths;
+use Config\Services;
+
+/**
+ * Bootstrap for the application
+ *
+ * @codeCoverageIgnore
+ */
+class Boot
+{
+ /**
+ * Used by `public/index.php`
+ *
+ * Context
+ * web: Invoked by HTTP request
+ * php-cli: Invoked by CLI via `php public/index.php`
+ *
+ * @return int Exit code.
+ */
+ public static function bootWeb(Paths $paths): int
+ {
+ static::definePathConstants($paths);
+ if (! defined('APP_NAMESPACE')) {
+ static::loadConstants();
+ }
+ static::checkMissingExtensions();
+
+ static::loadDotEnv($paths);
+ static::defineEnvironment();
+ static::loadEnvironmentBootstrap($paths);
+
+ static::loadCommonFunctions();
+ static::loadAutoloader();
+ static::setExceptionHandler();
+ static::initializeKint();
+
+ $configCacheEnabled = class_exists(Optimize::class)
+ && (new Optimize())->configCacheEnabled;
+ if ($configCacheEnabled) {
+ $factoriesCache = static::loadConfigCache();
+ }
+
+ static::autoloadHelpers();
+
+ $app = static::initializeCodeIgniter();
+ static::runCodeIgniter($app);
+
+ if ($configCacheEnabled) {
+ static::saveConfigCache($factoriesCache);
+ }
+
+ // Exits the application, setting the exit code for CLI-based
+ // applications that might be watching.
+ return EXIT_SUCCESS;
+ }
+
+ /**
+ * Used by `spark`
+ *
+ * @return int Exit code.
+ */
+ public static function bootSpark(Paths $paths): int
+ {
+ static::definePathConstants($paths);
+ if (! defined('APP_NAMESPACE')) {
+ static::loadConstants();
+ }
+ static::checkMissingExtensions();
+
+ static::loadDotEnv($paths);
+ static::defineEnvironment();
+ static::loadEnvironmentBootstrap($paths);
+
+ static::loadCommonFunctions();
+ static::loadAutoloader();
+ static::setExceptionHandler();
+ static::initializeKint();
+ static::autoloadHelpers();
+
+ static::initializeCodeIgniter();
+ $console = static::initializeConsole();
+
+ return static::runCommand($console);
+ }
+
+ /**
+ * Used by `system/Test/bootstrap.php`
+ */
+ public static function bootTest(Paths $paths): void
+ {
+ static::loadConstants();
+ static::checkMissingExtensions();
+
+ static::loadDotEnv($paths);
+ static::loadEnvironmentBootstrap($paths, false);
+
+ static::loadCommonFunctions();
+ static::loadAutoloader();
+ static::setExceptionHandler();
+ static::initializeKint();
+ static::autoloadHelpers();
+ }
+
+ /**
+ * Load environment settings from .env files into $_SERVER and $_ENV
+ */
+ protected static function loadDotEnv(Paths $paths): void
+ {
+ require_once $paths->systemDirectory . '/Config/DotEnv.php';
+ (new DotEnv($paths->appDirectory . '/../'))->load();
+ }
+
+ protected static function defineEnvironment(): void
+ {
+ if (! defined('ENVIRONMENT')) {
+ // @phpstan-ignore-next-line
+ $env = $_ENV['CI_ENVIRONMENT'] ?? $_SERVER['CI_ENVIRONMENT']
+ ?? getenv('CI_ENVIRONMENT')
+ ?: 'production';
+
+ define('ENVIRONMENT', $env);
+ }
+ }
+
+ protected static function loadEnvironmentBootstrap(Paths $paths, bool $exit = true): void
+ {
+ if (is_file($paths->appDirectory . '/Config/Boot/' . ENVIRONMENT . '.php')) {
+ require_once $paths->appDirectory . '/Config/Boot/' . ENVIRONMENT . '.php';
+
+ return;
+ }
+
+ if ($exit) {
+ header('HTTP/1.1 503 Service Unavailable.', true, 503);
+ echo 'The application environment is not set correctly.';
+
+ exit(EXIT_ERROR);
+ }
+ }
+
+ /**
+ * The path constants provide convenient access to the folders throughout
+ * the application. We have to set them up here, so they are available in
+ * the config files that are loaded.
+ */
+ protected static function definePathConstants(Paths $paths): void
+ {
+ // The path to the application directory.
+ if (! defined('APPPATH')) {
+ define('APPPATH', realpath(rtrim($paths->appDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
+ }
+
+ // The path to the project root directory. Just above APPPATH.
+ if (! defined('ROOTPATH')) {
+ define('ROOTPATH', realpath(APPPATH . '../') . DIRECTORY_SEPARATOR);
+ }
+
+ // The path to the system directory.
+ if (! defined('SYSTEMPATH')) {
+ define('SYSTEMPATH', realpath(rtrim($paths->systemDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
+ }
+
+ // The path to the writable directory.
+ if (! defined('WRITEPATH')) {
+ define('WRITEPATH', realpath(rtrim($paths->writableDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
+ }
+
+ // The path to the tests directory
+ if (! defined('TESTPATH')) {
+ define('TESTPATH', realpath(rtrim($paths->testsDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
+ }
+ }
+
+ protected static function loadConstants(): void
+ {
+ require_once APPPATH . 'Config/Constants.php';
+ }
+
+ protected static function loadCommonFunctions(): void
+ {
+ // Require app/Common.php file if exists.
+ if (is_file(APPPATH . 'Common.php')) {
+ require_once APPPATH . 'Common.php';
+ }
+
+ // Require system/Common.php
+ require_once SYSTEMPATH . 'Common.php';
+ }
+
+ /**
+ * The autoloader allows all the pieces to work together in the framework.
+ * We have to load it here, though, so that the config files can use the
+ * path constants.
+ */
+ protected static function loadAutoloader(): void
+ {
+ if (! class_exists(Autoload::class, false)) {
+ require_once SYSTEMPATH . 'Config/AutoloadConfig.php';
+ require_once APPPATH . 'Config/Autoload.php';
+ require_once SYSTEMPATH . 'Modules/Modules.php';
+ require_once APPPATH . 'Config/Modules.php';
+ }
+
+ require_once SYSTEMPATH . 'Autoloader/Autoloader.php';
+ require_once SYSTEMPATH . 'Config/BaseService.php';
+ require_once SYSTEMPATH . 'Config/Services.php';
+ require_once APPPATH . 'Config/Services.php';
+
+ // Initialize and register the loader with the SPL autoloader stack.
+ Services::autoloader()->initialize(new Autoload(), new Modules())->register();
+ }
+
+ protected static function autoloadHelpers(): void
+ {
+ Services::autoloader()->loadHelpers();
+ }
+
+ protected static function setExceptionHandler(): void
+ {
+ Services::exceptions()->initialize();
+ }
+
+ protected static function checkMissingExtensions(): void
+ {
+ if (is_file(COMPOSER_PATH)) {
+ return;
+ }
+
+ // Run this check for manual installations
+ $missingExtensions = [];
+
+ foreach ([
+ 'intl',
+ 'json',
+ 'mbstring',
+ ] as $extension) {
+ if (! extension_loaded($extension)) {
+ $missingExtensions[] = $extension;
+ }
+ }
+
+ if ($missingExtensions === []) {
+ return;
+ }
+
+ $message = sprintf(
+ 'The framework needs the following extension(s) installed and loaded: %s.',
+ implode(', ', $missingExtensions)
+ );
+
+ header('HTTP/1.1 503 Service Unavailable.', true, 503);
+ echo $message;
+
+ exit(EXIT_ERROR);
+ }
+
+ protected static function initializeKint(): void
+ {
+ Services::autoloader()->initializeKint(CI_DEBUG);
+ }
+
+ protected static function loadConfigCache(): FactoriesCache
+ {
+ $factoriesCache = new FactoriesCache();
+ $factoriesCache->load('config');
+
+ return $factoriesCache;
+ }
+
+ /**
+ * The CodeIgniter class contains the core functionality to make
+ * the application run, and does all the dirty work to get
+ * the pieces all working together.
+ */
+ protected static function initializeCodeIgniter(): CodeIgniter
+ {
+ $app = Config\Services::codeigniter();
+ $app->initialize();
+ $context = is_cli() ? 'php-cli' : 'web';
+ $app->setContext($context);
+
+ return $app;
+ }
+
+ /**
+ * Now that everything is set up, it's time to actually fire
+ * up the engines and make this app do its thang.
+ */
+ protected static function runCodeIgniter(CodeIgniter $app): void
+ {
+ $app->run();
+ }
+
+ protected static function saveConfigCache(FactoriesCache $factoriesCache): void
+ {
+ $factoriesCache->save('config');
+ }
+
+ protected static function initializeConsole(): Console
+ {
+ $console = new Console();
+
+ // Show basic information before we do anything else.
+ // @phpstan-ignore-next-line
+ if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) {
+ unset($_SERVER['argv'][$suppress]); // @phpstan-ignore-line
+ $suppress = true;
+ }
+
+ $console->showHeader($suppress);
+
+ return $console;
+ }
+
+ protected static function runCommand(Console $console): int
+ {
+ $exit = $console->run();
+
+ return is_int($exit) ? $exit : EXIT_SUCCESS;
+ }
+}
diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php
index 8101283b908a..1b273846bd74 100644
--- a/system/CLI/BaseCommand.php
+++ b/system/CLI/BaseCommand.php
@@ -1,5 +1,7 @@
input($prefix);
}
/**
@@ -220,13 +215,11 @@ public static function input(?string $prefix = null): string
* // Do not provide options but requires a valid email
* $email = CLI::prompt('What is your email?', null, 'required|valid_email');
*
- * @param string $field Output "field" question
- * @param array|string $options String to a default value, array to a list of options (the first option will be the default value)
- * @param array|string|null $validation Validation rules
+ * @param string $field Output "field" question
+ * @param list|string $options String to a default value, array to a list of options (the first option will be the default value)
+ * @param array|string|null $validation Validation rules
*
* @return string The user input
- *
- * @codeCoverageIgnore
*/
public static function prompt(string $field, $options = null, $validation = null): string
{
@@ -246,9 +239,9 @@ public static function prompt(string $field, $options = null, $validation = null
$default = $options;
}
- if (is_array($options) && $options) {
+ if (is_array($options) && $options !== []) {
$opts = $options;
- $extraOutputDefault = static::color($opts[0], 'green');
+ $extraOutputDefault = static::color((string) $opts[0], 'green');
unset($opts[0]);
@@ -265,7 +258,7 @@ public static function prompt(string $field, $options = null, $validation = null
static::fwrite(STDOUT, $field . (trim($field) !== '' ? ' ' : '') . $extraOutput . ': ');
// Read the input from keyboard.
- $input = trim(static::input()) ?: $default;
+ $input = trim(static::$io->input()) ?: (string) $default;
if ($validation !== []) {
while (! static::validate('"' . trim($field) . '"', $input, $validation)) {
@@ -285,8 +278,6 @@ public static function prompt(string $field, $options = null, $validation = null
* @param array|string|null $validation Validation rules
*
* @return string The selected key of $options
- *
- * @codeCoverageIgnore
*/
public static function promptByKey($text, array $options, $validation = null): string
{
@@ -415,8 +406,6 @@ private static function printKeysAndValues(array $options): void
* @param string $field Prompt "field" output
* @param string $value Input value
* @param array|string $rules Validation rules
- *
- * @codeCoverageIgnore
*/
protected static function validate(string $field, string $value, $rules): bool
{
@@ -533,11 +522,8 @@ public static function wait(int $seconds, bool $countdown = false)
} elseif ($seconds > 0) {
sleep($seconds);
} else {
- // this chunk cannot be tested because of keyboard input
- // @codeCoverageIgnoreStart
static::write(static::$wait_msg);
- static::input();
- // @codeCoverageIgnoreEnd
+ static::$io->input();
}
}
@@ -567,8 +553,6 @@ public static function newLine(int $num = 1)
/**
* Clears the screen of output
*
- * @codeCoverageIgnore
- *
* @return void
*/
public static function clearScreen()
@@ -608,7 +592,7 @@ public static function color(string $text, string $foreground, ?string $backgrou
$newText = '';
// Detect if color method was already in use with this text
- if (strpos($text, "\033[0m") !== false) {
+ if (str_contains($text, "\033[0m")) {
$pattern = '/\\033\\[0;.+?\\033\\[0m/u';
preg_match_all($pattern, $text, $matches);
@@ -762,8 +746,6 @@ public static function getHeight(int $default = 32): int
/**
* Populates the CLI's dimensions.
*
- * @codeCoverageIgnore
- *
* @return void
*/
public static function generateDimensions()
@@ -1066,7 +1048,7 @@ public static function table(array $tbody, array $thead = [])
foreach ($tableRows[$row] as $col) {
// Sets the size of this column in the current row
- $allColsLengths[$row][$column] = static::strlen($col);
+ $allColsLengths[$row][$column] = static::strlen((string) $col);
// If the current column does not have a value among the larger ones
// or the value of this is greater than the existing one
@@ -1086,7 +1068,7 @@ public static function table(array $tbody, array $thead = [])
$column = 0;
foreach ($tableRows[$row] as $col) {
- $diff = $maxColsLengths[$column] - static::strlen($col);
+ $diff = $maxColsLengths[$column] - static::strlen((string) $col);
if ($diff !== 0) {
$tableRows[$row][$column] .= str_repeat(' ', $diff);
@@ -1106,7 +1088,7 @@ public static function table(array $tbody, array $thead = [])
$cols = '+';
foreach ($tableRows[$row] as $col) {
- $cols .= str_repeat('-', static::strlen($col) + 2) . '+';
+ $cols .= str_repeat('-', static::strlen((string) $col) + 2) . '+';
}
$table .= $cols . PHP_EOL;
}
@@ -1137,15 +1119,27 @@ public static function table(array $tbody, array $thead = [])
*/
protected static function fwrite($handle, string $string)
{
- if (! is_cli()) {
- // @codeCoverageIgnoreStart
- echo $string;
+ static::$io->fwrite($handle, $string);
+ }
- return;
- // @codeCoverageIgnoreEnd
- }
+ /**
+ * Testing purpose only
+ *
+ * @testTag
+ */
+ public static function setInputOutput(InputOutput $io): void
+ {
+ static::$io = $io;
+ }
- fwrite($handle, $string);
+ /**
+ * Testing purpose only
+ *
+ * @testTag
+ */
+ public static function resetInputOutput(): void
+ {
+ static::$io = new InputOutput();
}
}
diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php
index 1de0df2791d6..30bd2c2652cd 100644
--- a/system/CLI/Commands.php
+++ b/system/CLI/Commands.php
@@ -1,5 +1,7 @@
commands[$command]['class'];
$class = new $className($this->logger, $this);
- return $class->run($params);
+ Events::trigger('pre_command');
+
+ $exit = $class->run($params);
+
+ Events::trigger('post_command');
+
+ return $exit;
}
/**
@@ -87,7 +96,7 @@ public function discoverCommands()
return;
}
- /** @var FileLocator $locator */
+ /** @var FileLocatorInterface $locator */
$locator = service('locator');
$files = $locator->listFiles('Commands/');
@@ -100,9 +109,9 @@ public function discoverCommands()
// Loop over each file checking to see if a command with that
// alias exists in the class.
foreach ($files as $file) {
- $className = $locator->getClassname($file);
+ $className = $locator->findQualifiedNameFromPath($file);
- if ($className === '' || ! class_exists($className)) {
+ if ($className === false || ! class_exists($className)) {
continue;
}
@@ -174,7 +183,7 @@ protected function getCommandAlternatives(string $name, array $collection): arra
foreach (array_keys($collection) as $commandName) {
$lev = levenshtein($name, $commandName);
- if ($lev <= strlen($commandName) / 3 || strpos($commandName, $name) !== false) {
+ if ($lev <= strlen($commandName) / 3 || str_contains($commandName, $name)) {
$alternatives[$commandName] = $lev;
}
}
diff --git a/system/CLI/Console.php b/system/CLI/Console.php
index adcf9aaeca8b..725193d424b5 100644
--- a/system/CLI/Console.php
+++ b/system/CLI/Console.php
@@ -1,5 +1,7 @@
params = $params;
@@ -109,7 +124,7 @@ protected function generateClass(array $params)
$target = $this->buildPath($class);
// Check if path is empty.
- if (empty($target)) {
+ if ($target === '') {
return;
}
@@ -118,15 +133,17 @@ protected function generateClass(array $params)
/**
* Generate a view file from an existing template.
+ *
+ * @param string $view namespaced view name that is generated
*/
- protected function generateView(string $view, array $params)
+ protected function generateView(string $view, array $params): void
{
$this->params = $params;
$target = $this->buildPath($view);
// Check if path is empty.
- if (empty($target)) {
+ if ($target === '') {
return;
}
@@ -135,6 +152,8 @@ protected function generateView(string $view, array $params)
/**
* Handles writing the file to disk, and all of the safety checks around that.
+ *
+ * @param string $target file path
*/
private function generateFile(string $target, string $content): void
{
@@ -143,7 +162,13 @@ private function generateFile(string $target, string $content): void
CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');
CLI::newLine();
- if (CLI::prompt('Are you sure you want to continue?', ['y', 'n'], 'required') === 'n') {
+ if (
+ CLI::prompt(
+ 'Are you sure you want to continue?',
+ ['y', 'n'],
+ 'required'
+ ) === 'n'
+ ) {
CLI::newLine();
CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');
CLI::newLine();
@@ -160,7 +185,11 @@ private function generateFile(string $target, string $content): void
// Overwriting files unknowingly is a serious annoyance, So we'll check if
// we are duplicating things, If 'force' option is not supplied, we bail.
if (! $this->getOption('force') && $isFile) {
- CLI::error(lang('CLI.generator.fileExist', [clean_path($target)]), 'light_gray', 'red');
+ CLI::error(
+ lang('CLI.generator.fileExist', [clean_path($target)]),
+ 'light_gray',
+ 'red'
+ );
CLI::newLine();
return;
@@ -179,7 +208,11 @@ private function generateFile(string $target, string $content): void
// contents from the template, and then we'll do the necessary replacements.
if (! write_file($target, $content)) {
// @codeCoverageIgnoreStart
- CLI::error(lang('CLI.generator.fileError', [clean_path($target)]), 'light_gray', 'red');
+ CLI::error(
+ lang('CLI.generator.fileError', [clean_path($target)]),
+ 'light_gray',
+ 'red'
+ );
CLI::newLine();
return;
@@ -187,18 +220,28 @@ private function generateFile(string $target, string $content): void
}
if ($this->getOption('force') && $isFile) {
- CLI::write(lang('CLI.generator.fileOverwrite', [clean_path($target)]), 'yellow');
+ CLI::write(
+ lang('CLI.generator.fileOverwrite', [clean_path($target)]),
+ 'yellow'
+ );
CLI::newLine();
return;
}
- CLI::write(lang('CLI.generator.fileCreate', [clean_path($target)]), 'green');
+ CLI::write(
+ lang('CLI.generator.fileCreate', [clean_path($target)]),
+ 'green'
+ );
CLI::newLine();
}
/**
* Prepare options and do the necessary replacements.
+ *
+ * @param string $class namespaced classname or namespaced view.
+ *
+ * @return string generated file content
*/
protected function prepare(string $class): string
{
@@ -219,14 +262,35 @@ protected function basename(string $filename): string
* Parses the class name and checks if it is already qualified.
*/
protected function qualifyClassName(): string
+ {
+ $class = $this->normalizeInputClassName();
+
+ // Gets the namespace from input. Don't forget the ending backslash!
+ $namespace = $this->getNamespace() . '\\';
+
+ if (str_starts_with($class, $namespace)) {
+ return $class; // @codeCoverageIgnore
+ }
+
+ $directoryString = ($this->directory !== null) ? $this->directory . '\\' : '';
+
+ return $namespace . $directoryString . str_replace('/', '\\', $class);
+ }
+
+ /**
+ * Normalize input classname.
+ */
+ private function normalizeInputClassName(): string
{
// Gets the class name from input.
$class = $this->params[0] ?? CLI::getSegment(2);
if ($class === null && $this->hasClassName) {
// @codeCoverageIgnoreStart
- $nameLang = $this->classNameLang ?: 'CLI.generator.className.default';
- $class = CLI::prompt(lang($nameLang), null, 'required');
+ $nameLang = $this->classNameLang !== ''
+ ? $this->classNameLang
+ : 'CLI.generator.className.default';
+ $class = CLI::prompt(lang($nameLang), null, 'required');
CLI::newLine();
// @codeCoverageIgnoreEnd
}
@@ -244,21 +308,24 @@ protected function qualifyClassName(): string
$class = $matches[1] . ucfirst($matches[2]);
}
- if ($this->enabledSuffixing && $this->getOption('suffix') && preg_match($pattern, $class) !== 1) {
+ if (
+ $this->enabledSuffixing && $this->getOption('suffix')
+ && preg_match($pattern, $class) !== 1
+ ) {
$class .= ucfirst($component);
}
// Trims input, normalize separators, and ensure that all paths are in Pascalcase.
- $class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/');
-
- // Gets the namespace from input. Don't forget the ending backslash!
- $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\') . '\\';
-
- if (strncmp($class, $namespace, strlen($namespace)) === 0) {
- return $class; // @codeCoverageIgnore
- }
-
- return $namespace . $this->directory . '\\' . str_replace('/', '\\', $class);
+ return ltrim(
+ implode(
+ '\\',
+ array_map(
+ 'pascalize',
+ explode('\\', str_replace('/', '\\', trim($class)))
+ )
+ ),
+ '\\/'
+ );
}
/**
@@ -268,21 +335,41 @@ protected function qualifyClassName(): string
protected function renderTemplate(array $data = []): string
{
try {
- return view(config(Generators::class)->views[$this->name], $data, ['debug' => false]);
+ $template = $this->templatePath ?? config(Generators::class)->views[$this->name];
+
+ return view($template, $data, ['debug' => false]);
} catch (Throwable $e) {
log_message('error', (string) $e);
- return view("CodeIgniter\\Commands\\Generators\\Views\\{$this->template}", $data, ['debug' => false]);
+ return view(
+ "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}",
+ $data,
+ ['debug' => false]
+ );
}
}
/**
* Performs pseudo-variables contained within view file.
+ *
+ * @param string $class namespaced classname or namespaced view.
+ *
+ * @return string generated file content
*/
- protected function parseTemplate(string $class, array $search = [], array $replace = [], array $data = []): string
- {
+ protected function parseTemplate(
+ string $class,
+ array $search = [],
+ array $replace = [],
+ array $data = []
+ ): string {
// Retrieves the namespace part from the fully qualified class name.
- $namespace = trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\');
+ $namespace = trim(
+ implode(
+ '\\',
+ array_slice(explode('\\', $class), 0, -1)
+ ),
+ '\\'
+ );
$search[] = '<@php';
$search[] = '{namespace}';
$search[] = '{class}';
@@ -302,7 +389,14 @@ protected function buildContent(string $class): string
{
$template = $this->prepare($class);
- if ($this->sortImports && preg_match('/(?P(?:^use [^;]+;$\n?)+)/m', $template, $match)) {
+ if (
+ $this->sortImports
+ && preg_match(
+ '/(?P(?:^use [^;]+;$\n?)+)/m',
+ $template,
+ $match
+ )
+ ) {
$imports = explode("\n", trim($match['imports']));
sort($imports);
@@ -314,25 +408,62 @@ protected function buildContent(string $class): string
/**
* Builds the file path from the class name.
+ *
+ * @param string $class namespaced classname or namespaced view.
*/
protected function buildPath(string $class): string
{
- $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\');
+ $namespace = $this->getNamespace();
// Check if the namespace is actually defined and we are not just typing gibberish.
- $base = Services::autoloader()->getNamespace($namespace);
+ $base = service('autoloader')->getNamespace($namespace);
if (! $base = reset($base)) {
- CLI::error(lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red');
+ CLI::error(
+ lang('CLI.namespaceNotDefined', [$namespace]),
+ 'light_gray',
+ 'red'
+ );
CLI::newLine();
return '';
}
- $base = realpath($base) ?: $base;
- $file = $base . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, trim(str_replace($namespace . '\\', '', $class), '\\')) . '.php';
+ $realpath = realpath($base);
+ $base = ($realpath !== false) ? $realpath : $base;
+
+ $file = $base . DIRECTORY_SEPARATOR
+ . str_replace(
+ '\\',
+ DIRECTORY_SEPARATOR,
+ trim(str_replace($namespace . '\\', '', $class), '\\')
+ ) . '.php';
+
+ return implode(
+ DIRECTORY_SEPARATOR,
+ array_slice(
+ explode(DIRECTORY_SEPARATOR, $file),
+ 0,
+ -1
+ )
+ ) . DIRECTORY_SEPARATOR . $this->basename($file);
+ }
- return implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, -1)) . DIRECTORY_SEPARATOR . $this->basename($file);
+ /**
+ * Gets the namespace from the command-line option,
+ * or the default namespace if the option is not set.
+ * Can be overridden by directly setting $this->namespace.
+ */
+ protected function getNamespace(): string
+ {
+ return $this->namespace ?? trim(
+ str_replace(
+ '/',
+ '\\',
+ $this->getOption('namespace') ?? APP_NAMESPACE
+ ),
+ '\\'
+ );
}
/**
@@ -374,10 +505,8 @@ protected function setEnabledSuffixing(bool $enabledSuffixing)
/**
* Gets a single command-line option. Returns TRUE if the option exists,
* but doesn't have a value, and is simply acting as a flag.
- *
- * @return mixed
*/
- protected function getOption(string $name)
+ protected function getOption(string $name): string|bool|null
{
if (! array_key_exists($name, $this->params)) {
return CLI::getOption($name);
diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php
new file mode 100644
index 000000000000..b69c19e2eee1
--- /dev/null
+++ b/system/CLI/InputOutput.php
@@ -0,0 +1,80 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\CLI;
+
+/**
+ * Input and Output for CLI.
+ */
+class InputOutput
+{
+ /**
+ * Is the readline library on the system?
+ */
+ private readonly bool $readlineSupport;
+
+ public function __construct()
+ {
+ // Readline is an extension for PHP that makes interactivity with PHP
+ // much more bash-like.
+ // http://www.php.net/manual/en/readline.installation.php
+ $this->readlineSupport = extension_loaded('readline');
+ }
+
+ /**
+ * Get input from the shell, using readline or the standard STDIN
+ *
+ * Named options must be in the following formats:
+ * php index.php user -v --v -name=John --name=John
+ *
+ * @param string|null $prefix You may specify a string with which to prompt the user.
+ */
+ public function input(?string $prefix = null): string
+ {
+ // readline() can't be tested.
+ if ($this->readlineSupport && ENVIRONMENT !== 'testing') {
+ return readline($prefix); // @codeCoverageIgnore
+ }
+
+ echo $prefix;
+
+ $input = fgets(fopen('php://stdin', 'rb'));
+
+ if ($input === false) {
+ $input = '';
+ }
+
+ return $input;
+ }
+
+ /**
+ * While the library is intended for use on CLI commands,
+ * commands can be called from controllers and elsewhere
+ * so we need a way to allow them to still work.
+ *
+ * For now, just echo the content, but look into a better
+ * solution down the road.
+ *
+ * @param resource $handle
+ */
+ public function fwrite($handle, string $string): void
+ {
+ if (! is_cli()) {
+ echo $string;
+
+ return;
+ }
+
+ fwrite($handle, $string);
+ }
+}
diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php
index 6402b002866d..fb60d73c5cd7 100644
--- a/system/Cache/CacheFactory.php
+++ b/system/Cache/CacheFactory.php
@@ -1,5 +1,7 @@
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Cache\Exceptions;
-
-/**
- * Provides a domain-level interface for broad capture
- * of all framework-related exceptions.
- *
- * catch (\CodeIgniter\Cache\Exceptions\ExceptionInterface) { ... }
- *
- * @deprecated 4.1.2
- */
-interface ExceptionInterface
-{
-}
diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php
index d78d0b1b02a4..e4b7488f43df 100644
--- a/system/Cache/FactoriesCache.php
+++ b/system/Cache/FactoriesCache.php
@@ -1,5 +1,7 @@
unserialize($data['__ci_value']),
+ // Yes, 'double' is returned and NOT 'float'
+ 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
+ default => null,
+ };
}
/**
diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php
index 953de2dc20be..9e1003d10ac2 100644
--- a/system/Cache/Handlers/RedisHandler.php
+++ b/system/Cache/Handlers/RedisHandler.php
@@ -1,5 +1,7 @@
unserialize($data['__ci_value']),
+ // Yes, 'double' is returned and NOT 'float'
+ 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
+ default => null,
+ };
}
/**
diff --git a/system/Cache/Handlers/WincacheHandler.php b/system/Cache/Handlers/WincacheHandler.php
index b1ea45ded7a2..0ddee50a7fde 100644
--- a/system/Cache/Handlers/WincacheHandler.php
+++ b/system/Cache/Handlers/WincacheHandler.php
@@ -1,5 +1,7 @@
cacheQueryString = $config->cacheQueryString;
- $this->cache = $cache;
}
/**
@@ -83,7 +83,7 @@ public function generateCacheKey($request): string
? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : [])
: '';
- return md5($uri->setFragment('')->setQuery($query));
+ return md5($request->getMethod() . ':' . $uri->setFragment('')->setQuery($query));
}
/**
@@ -99,8 +99,14 @@ public function make($request, ResponseInterface $response): bool
$headers = [];
- foreach ($response->headers() as $header) {
- $headers[$header->getName()] = $header->getValueLine();
+ foreach ($response->headers() as $name => $value) {
+ if ($value instanceof Header) {
+ $headers[$name] = $value->getValueLine();
+ } else {
+ foreach ($value as $header) {
+ $headers[$name][] = $header->getValueLine();
+ }
+ }
}
return $this->cache->save(
diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php
index 6a2dca68c1e5..7fdade26fb99 100644
--- a/system/CodeIgniter.php
+++ b/system/CodeIgniter.php
@@ -17,10 +17,12 @@
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\Filters\Filters;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\HTTP\IncomingRequest;
+use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\ResponsableInterface;
@@ -54,7 +56,7 @@ class CodeIgniter
/**
* The current version of CodeIgniter Framework
*/
- public const CI_VERSION = '4.4.8';
+ public const CI_VERSION = '4.5.0';
/**
* App startup time.
@@ -135,25 +137,6 @@ class CodeIgniter
*/
protected static $cacheTTL = 0;
- /**
- * Request path to use.
- *
- * @var string|null
- *
- * @deprecated No longer used.
- */
- protected $path;
-
- /**
- * Should the Response instance "pretend"
- * to keep from setting headers/cookies/etc
- *
- * @var bool
- *
- * @deprecated No longer used.
- */
- protected $useSafeOutput = false;
-
/**
* Context
* web: Invoked by HTTP request
@@ -171,7 +154,7 @@ class CodeIgniter
/**
* Whether to return Response object or send response.
*
- * @deprecated No longer used.
+ * @deprecated 4.4.0 No longer used.
*/
protected bool $returnResponse = false;
@@ -203,24 +186,11 @@ public function __construct(App $config)
*/
public function initialize()
{
- // Define environment variables
- $this->bootstrapEnvironment();
-
- // Setup Exception Handling
- Services::exceptions()->initialize();
-
- // Run this check for manual installations
- if (! is_file(COMPOSER_PATH)) {
- $this->resolvePlatformExtensions(); // @codeCoverageIgnore
- }
-
// Set default locale on the server
Locale::setDefault($this->config->defaultLocale ?? 'en');
// Set default timezone on the server
date_default_timezone_set($this->config->appTimezone ?? 'UTC');
-
- $this->initializeKint();
}
/**
@@ -231,6 +201,8 @@ public function initialize()
* @throws FrameworkException
*
* @codeCoverageIgnore
+ *
+ * @deprecated 4.5.0 Moved to system/bootstrap.php.
*/
protected function resolvePlatformExtensions()
{
@@ -257,6 +229,8 @@ protected function resolvePlatformExtensions()
* Initializes Kint
*
* @return void
+ *
+ * @deprecated 4.5.0 Moved to Autoloader.
*/
protected function initializeKint()
{
@@ -272,6 +246,9 @@ protected function initializeKint()
helper('kint');
}
+ /**
+ * @deprecated 4.5.0 Moved to Autoloader.
+ */
private function autoloadKint(): void
{
// If we have KINT_DIR it means it's already loaded via composer
@@ -294,9 +271,12 @@ private function autoloadKint(): void
}
}
+ /**
+ * @deprecated 4.5.0 Moved to Autoloader.
+ */
private function configureKint(): void
{
- $config = config(KintConfig::class);
+ $config = new KintConfig();
Kint::$depth_limit = $config->maxDepth;
Kint::$display_called_from = $config->displayCalledFrom;
@@ -336,6 +316,8 @@ private function configureKint(): void
* tries to route the response, loads the controller and generally
* makes all the pieces work together.
*
+ * @param bool $returnResponse Used for testing purposes only.
+ *
* @return ResponseInterface|void
*/
public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
@@ -355,25 +337,43 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
$this->getRequestObject();
$this->getResponseObject();
- $this->spoofRequestMethod();
+ Events::trigger('pre_system');
- try {
- $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
- } catch (ResponsableInterface|DeprecatedRedirectException $e) {
- $this->outputBufferingEnd();
- if ($e instanceof DeprecatedRedirectException) {
- $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
- }
+ $this->benchmark->stop('bootstrap');
+
+ $this->benchmark->start('required_before_filters');
+ // Start up the filters
+ $filters = Services::filters();
+ // Run required before filters
+ $possibleResponse = $this->runRequiredBeforeFilters($filters);
- $this->response = $e->getResponse();
- } catch (PageNotFoundException $e) {
- $this->response = $this->display404errors($e);
- } catch (Throwable $e) {
- $this->outputBufferingEnd();
+ // If a ResponseInterface instance is returned then send it back to the client and stop
+ if ($possibleResponse instanceof ResponseInterface) {
+ $this->response = $possibleResponse;
+ } else {
+ try {
+ $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
+ } catch (ResponsableInterface|DeprecatedRedirectException $e) {
+ $this->outputBufferingEnd();
+ if ($e instanceof DeprecatedRedirectException) {
+ $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ $this->response = $e->getResponse();
+ } catch (PageNotFoundException $e) {
+ $this->response = $this->display404errors($e);
+ } catch (Throwable $e) {
+ $this->outputBufferingEnd();
- throw $e;
+ throw $e;
+ }
}
+ $this->runRequiredAfterFilters($filters);
+
+ // Is there a post-system event?
+ Events::trigger('post_system');
+
if ($returnResponse) {
return $this->response;
}
@@ -382,19 +382,36 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
}
/**
- * Set our Response instance to "pretend" mode so that things like
- * cookies and headers are not actually sent, allowing PHP 7.2+ to
- * not complain when ini_set() function is used.
- *
- * @return $this
- *
- * @deprecated No longer used.
+ * Run required before filters.
*/
- public function useSafeOutput(bool $safe = true)
+ private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
{
- $this->useSafeOutput = $safe;
+ $possibleResponse = $filters->runRequired('before');
+ $this->benchmark->stop('required_before_filters');
- return $this;
+ // If a ResponseInterface instance is returned then send it back to the client and stop
+ if ($possibleResponse instanceof ResponseInterface) {
+ return $possibleResponse;
+ }
+
+ return null;
+ }
+
+ /**
+ * Run required after filters.
+ */
+ private function runRequiredAfterFilters(Filters $filters): void
+ {
+ $filters->setResponse($this->response);
+
+ // Run required after filters
+ $this->benchmark->start('required_after_filters');
+ $response = $filters->runRequired('after');
+ $this->benchmark->stop('required_after_filters');
+
+ if ($response instanceof ResponseInterface) {
+ $this->response = $response;
+ }
}
/**
@@ -433,41 +450,30 @@ public function disableFilters(): void
*/
protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
{
- $this->forceSecureAccess();
-
- if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') {
+ if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
}
- Events::trigger('pre_system');
-
- // Check for a cached page. Execution will stop
- // if the page has been cached.
- if (($response = $this->displayCache($cacheConfig)) instanceof ResponseInterface) {
- return $response;
- }
-
- $routeFilter = $this->tryToRouteIt($routes);
+ $routeFilters = $this->tryToRouteIt($routes);
// $uri is URL-encoded.
- $uri = $this->determinePath();
+ $uri = $this->request->getPath();
if ($this->enableFilters) {
- // Start up the filters
- $filters = Services::filters();
+ /** @var Filters $filters */
+ $filters = service('filters');
// If any filters were specified within the routes file,
// we need to ensure it's active for the current request
- if ($routeFilter !== null) {
- $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false;
- if ($multipleFiltersEnabled) {
- $filters->enableFilters($routeFilter, 'before');
- $filters->enableFilters($routeFilter, 'after');
- } else {
- // for backward compatibility
- $filters->enableFilter($routeFilter, 'before');
- $filters->enableFilter($routeFilter, 'after');
+ if ($routeFilters !== null) {
+ $filters->enableFilters($routeFilters, 'before');
+
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+ if (! $oldFilterOrder) {
+ $routeFilters = array_reverse($routeFilters);
}
+
+ $filters->enableFilters($routeFilters, 'after');
}
// Run "before" filters
@@ -512,12 +518,10 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
$this->gatherOutput($cacheConfig, $returned);
if ($this->enableFilters) {
- $filters = Services::filters();
+ /** @var Filters $filters */
+ $filters = service('filters');
$filters->setResponse($this->response);
- // After filter debug toolbar requires 'total_execution'.
- $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
-
// Run "after" filters
$this->benchmark->start('after_filters');
$response = $filters->run($uri, 'after');
@@ -533,18 +537,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
! $this->response instanceof DownloadResponse
&& ! $this->response instanceof RedirectResponse
) {
- // Cache it without the performance metrics replaced
- // so that we can have live speed updates along the way.
- // Must be run after filters to preserve the Response headers.
- $this->pageCache->make($this->request, $this->response);
-
- // Update the performance metrics
- $body = $this->response->getBody();
- if ($body !== null) {
- $output = $this->displayPerformanceMetrics($body);
- $this->response->setBody($output);
- }
-
// Save our current URI as the previous URI in the session
// for safer, more accurate use with `previous_url()` helper function.
$this->storePreviousURL(current_url(true));
@@ -552,9 +544,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
unset($uri);
- // Is there a post-system event?
- Events::trigger('post_system');
-
return $this->response;
}
@@ -590,6 +579,8 @@ protected function detectEnvironment()
* is wrong. At the very least, they should have error reporting setup.
*
* @return void
+ *
+ * @deprecated 4.5.0 Moved to system/bootstrap.php.
*/
protected function bootstrapEnvironment()
{
@@ -631,6 +622,9 @@ protected function startBenchmark()
* @param CLIRequest|IncomingRequest $request
*
* @return $this
+ *
+ * @internal Used for testing purposes only.
+ * @testTag
*/
public function setRequest($request)
{
@@ -647,6 +641,8 @@ public function setRequest($request)
protected function getRequestObject()
{
if ($this->request instanceof Request) {
+ $this->spoofRequestMethod();
+
return;
}
@@ -656,7 +652,9 @@ protected function getRequestObject()
Services::createRequest($this->config);
}
- $this->request = Services::request();
+ $this->request = service('request');
+
+ $this->spoofRequestMethod();
}
/**
@@ -688,6 +686,8 @@ protected function getResponseObject()
* should be enforced for this URL.
*
* @return void
+ *
+ * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
*/
protected function forceSecureAccess($duration = 31_536_000)
{
@@ -705,6 +705,7 @@ protected function forceSecureAccess($duration = 31_536_000)
*
* @throws Exception
*
+ * @deprecated 4.5.0 PageCache required filter is used. No longer used.
* @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
*/
public function displayCache(Cache $config)
@@ -759,6 +760,9 @@ public function cachePage(Cache $config)
*/
public function getPerformanceStats(): array
{
+ // After filter debug toolbar requires 'total_execution'.
+ $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
+
return [
'startTime' => $this->startTime,
'totalTime' => $this->totalTime,
@@ -782,15 +786,21 @@ protected function generateCacheName(Cache $config): string
? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
: '';
- return md5($uri->setFragment('')->setQuery($query));
+ return md5((string) $uri->setFragment('')->setQuery($query));
}
/**
- * Replaces the elapsed_time tag.
+ * Replaces the elapsed_time and memory_usage tag.
+ *
+ * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
*/
public function displayPerformanceMetrics(string $output): string
{
- return str_replace('{elapsed_time}', (string) $this->totalTime, $output);
+ return str_replace(
+ ['{elapsed_time}', '{memory_usage}'],
+ [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
+ $output
+ );
}
/**
@@ -807,22 +817,21 @@ public function displayPerformanceMetrics(string $output): string
*/
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
{
+ $this->benchmark->start('routing');
+
if ($routes === null) {
- $routes = Services::routes()->loadRoutes();
+ $routes = service('routes')->loadRoutes();
}
// $routes is defined in Config/Routes.php
$this->router = Services::router($routes, $this->request);
- // $path is URL-encoded.
- $path = $this->determinePath();
-
- $this->benchmark->stop('bootstrap');
- $this->benchmark->start('routing');
+ // $uri is URL-encoded.
+ $uri = $this->request->getPath();
$this->outputBufferingStart();
- $this->controller = $this->router->handle($path);
+ $this->controller = $this->router->handle($uri);
$this->method = $this->router->methodName();
// If a {locale} segment was matched in the final route,
@@ -833,12 +842,6 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
$this->benchmark->stop('routing');
- // for backward compatibility
- $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false;
- if (! $multipleFiltersEnabled) {
- return $this->router->getFilter();
- }
-
return $this->router->getFilters();
}
@@ -847,30 +850,12 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
* on the CLI/IncomingRequest path.
*
* @return string
- */
- protected function determinePath()
- {
- return $this->path ??
- (method_exists($this->request, 'getPath')
- ? $this->request->getPath()
- : $this->request->getUri()->getPath());
- }
-
- /**
- * Allows the request path to be set from outside the class,
- * instead of relying on CLIRequest or IncomingRequest for the path.
- *
- * This is not used now.
*
- * @return $this
- *
- * @deprecated No longer used.
+ * @deprecated 4.5.0 No longer used.
*/
- public function setPath(string $path)
+ protected function determinePath()
{
- $this->path = $path;
-
- return $this;
+ return $this->request->getPath();
}
/**
@@ -886,7 +871,7 @@ protected function startController()
$this->benchmark->start('controller_constructor');
// Is it routed to a Closure?
- if (is_object($this->controller) && (get_class($this->controller) === 'Closure')) {
+ if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
$controller = $this->controller;
return $controller(...$this->router->params());
@@ -898,7 +883,10 @@ protected function startController()
}
// Try to autoload the class
- if (! class_exists($this->controller, true) || $this->method[0] === '_') {
+ if (
+ ! class_exists($this->controller, true)
+ || ($this->method[0] === '_' && $this->method !== '__invoke')
+ ) {
throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
}
}
@@ -938,6 +926,8 @@ protected function runController($class)
// This is a Web request or PHP CLI request
$params = $this->router->params();
+ // The controller method param types may not be string.
+ // So cannot set `declare(strict_types=1)` in this file.
$output = method_exists($class, '_remap')
? $class->_remap($this->method, ...$params)
: $class->{$this->method}(...$params);
@@ -955,6 +945,8 @@ protected function runController($class)
*/
protected function display404errors(PageNotFoundException $e)
{
+ $this->response->setStatusCode($e->getCode());
+
// Is there a 404 Override available?
if ($override = $this->router->get404Override()) {
$returned = null;
@@ -969,7 +961,10 @@ protected function display404errors(PageNotFoundException $e)
$this->method = $override[1];
$controller = $this->createController();
- $returned = $this->runController($controller);
+
+ $returned = $controller->{$this->method}($e->getMessage());
+
+ $this->benchmark->stop('controller');
}
unset($override);
@@ -980,9 +975,6 @@ protected function display404errors(PageNotFoundException $e)
return $this->response;
}
- // Display 404 Errors
- $this->response->setStatusCode($e->getCode());
-
$this->outputBufferingEnd();
// Throws new PageNotFoundException and remove exception message on production.
@@ -1057,7 +1049,7 @@ public function storePreviousURL($uri)
}
// Ignore non-HTML responses
- if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) {
+ if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
return;
}
@@ -1086,7 +1078,7 @@ public function storePreviousURL($uri)
public function spoofRequestMethod()
{
// Only works with POSTED forms
- if (strtolower($this->request->getMethod()) !== 'post') {
+ if ($this->request->getMethod() !== Method::POST) {
return;
}
@@ -1097,7 +1089,7 @@ public function spoofRequestMethod()
}
// Only allows PUT, PATCH, DELETE
- if (in_array(strtoupper($method), ['PUT', 'PATCH', 'DELETE'], true)) {
+ if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
$this->request = $this->request->setMethod($method);
}
}
diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php
index 79fab9dfae3c..e1180c28c6bd 100644
--- a/system/Commands/Cache/ClearCache.php
+++ b/system/Commands/Cache/ClearCache.php
@@ -1,5 +1,7 @@
{$group}['database'] = $name;
if ($name !== ':memory:') {
- $dbName = strpos($name, DIRECTORY_SEPARATOR) === false ? WRITEPATH . $name : $name;
+ $dbName = ! str_contains($name, DIRECTORY_SEPARATOR) ? WRITEPATH . $name : $name;
if (is_file($dbName)) {
CLI::error("Database \"{$dbName}\" already exists.", 'light_gray', 'red');
diff --git a/system/Commands/Database/Migrate.php b/system/Commands/Database/Migrate.php
index 119335170d5e..b422e4a6db73 100644
--- a/system/Commands/Database/Migrate.php
+++ b/system/Commands/Database/Migrate.php
@@ -1,5 +1,7 @@
clearCliMessages();
CLI::write(lang('Migrations.latest'), 'yellow');
diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php
index f683219e9d2c..e5e8a6d967f9 100644
--- a/system/Commands/Database/MigrateRefresh.php
+++ b/system/Commands/Database/MigrateRefresh.php
@@ -1,5 +1,7 @@
getLastBatch() - 1;
diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php
index ecd074278d47..9506c2eae082 100644
--- a/system/Commands/Database/MigrateStatus.php
+++ b/system/Commands/Database/MigrateStatus.php
@@ -1,5 +1,7 @@
getNamespace();
+ $namespaces = service('autoloader')->getNamespace();
// Collection of migration status
$status = [];
@@ -115,7 +116,7 @@ public function run(array $params)
ksort($migrations);
foreach ($migrations as $uid => $migration) {
- $migrations[$uid]->name = mb_substr($migration->name, mb_strpos($migration->name, $uid . '_'));
+ $migrations[$uid]->name = mb_substr($migration->name, (int) mb_strpos($migration->name, $uid . '_'));
$date = '---';
$group = '---';
@@ -127,7 +128,7 @@ public function run(array $params)
continue;
}
- $date = date('Y-m-d H:i:s', $row->time);
+ $date = date('Y-m-d H:i:s', (int) $row->time);
$group = $row->group;
$batch = $row->batch;
// @codeCoverageIgnoreEnd
diff --git a/system/Commands/Database/Seed.php b/system/Commands/Database/Seed.php
index adfdd0a5f701..fc1c0f1ae62e 100644
--- a/system/Commands/Database/Seed.php
+++ b/system/Commands/Database/Seed.php
@@ -1,5 +1,7 @@
'Sorts the table rows in DESC order.',
'--limit-rows' => 'Limits the number of rows. Default: 10.',
'--limit-field-value' => 'Limits the length of field values. Default: 15.',
+ '--dbgroup' => 'Database group to show.',
];
/**
@@ -88,7 +92,7 @@ class ShowTableInfo extends BaseCommand
*/
private array $tbody;
- private BaseConnection $db;
+ private ?BaseConnection $db = null;
/**
* @var bool Sort the table rows in DESC order or not.
@@ -99,9 +103,20 @@ class ShowTableInfo extends BaseCommand
public function run(array $params)
{
- $this->db = Database::connect();
+ $dbGroup = $params['dbgroup'] ?? CLI::getOption('dbgroup');
+
+ try {
+ $this->db = Database::connect($dbGroup);
+ } catch (InvalidArgumentException $e) {
+ CLI::error($e->getMessage());
+
+ return EXIT_ERROR;
+ }
+
$this->DBPrefix = $this->db->getPrefix();
+ $this->showDBConfig();
+
$tables = $this->db->listTables();
if (array_key_exists('desc', $params)) {
@@ -112,13 +127,13 @@ public function run(array $params)
CLI::error('Database has no tables!', 'light_gray', 'red');
CLI::newLine();
- return;
+ return EXIT_ERROR;
}
if (array_key_exists('show', $params)) {
$this->showAllTables($tables);
- return;
+ return EXIT_ERROR;
}
$tableName = $params[0] ?? null;
@@ -139,10 +154,28 @@ public function run(array $params)
if (array_key_exists('metadata', $params)) {
$this->showFieldMetaData($tableName);
- return;
+ return EXIT_SUCCESS;
}
$this->showDataOfTable($tableName, $limitRows, $limitFieldValue);
+
+ return EXIT_SUCCESS;
+ }
+
+ private function showDBConfig(): void
+ {
+ $data = [[
+ 'hostname' => $this->db->hostname,
+ 'database' => $this->db->getDatabase(),
+ 'username' => $this->db->username,
+ 'DBDriver' => $this->db->getPlatform(),
+ 'DBPrefix' => $this->DBPrefix,
+ 'port' => $this->db->port,
+ ]];
+ CLI::table(
+ $data,
+ ['hostname', 'database', 'username', 'DBDriver', 'DBPrefix', 'port']
+ );
}
private function removeDBPrefix(): void
@@ -274,9 +307,12 @@ private function showFieldMetaData(string $tableName): void
CLI::table($this->tbody, $thead);
}
- private function setYesOrNo(bool $fieldValue): string
+ /**
+ * @param bool|int|string|null $fieldValue
+ */
+ private function setYesOrNo($fieldValue): string
{
- if ($fieldValue) {
+ if ((bool) $fieldValue) {
return CLI::color('Yes', 'green');
}
diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php
index 419ae6ef3e34..820ec48137c9 100644
--- a/system/Commands/Encryption/GenerateKey.php
+++ b/system/Commands/Encryption/GenerateKey.php
@@ -1,5 +1,7 @@
null]);
+ $this->templatePath = config(Generators::class)->views[$this->name]['class'];
$this->template = 'cell.tpl.php';
$this->classNameLang = 'CLI.generator.className.cell';
+
$this->generateClass($params);
- $this->name = 'make:cell_view';
+ $this->templatePath = config(Generators::class)->views[$this->name]['view'];
$this->template = 'cell_view.tpl.php';
$this->classNameLang = 'CLI.generator.viewName.cell';
$className = $this->qualifyClassName();
$viewName = decamelize(class_basename($className));
- $viewName = preg_replace('/([a-z][a-z0-9_\/\\\\]+)(_cell)$/i', '$1', $viewName) ?? $viewName;
+ $viewName = preg_replace(
+ '/([a-z][a-z0-9_\/\\\\]+)(_cell)$/i',
+ '$1',
+ $viewName
+ ) ?? $viewName;
$namespace = substr($className, 0, strrpos($className, '\\') + 1);
$this->generateView($namespace . $viewName, $params);
diff --git a/system/Commands/Generators/CommandGenerator.php b/system/Commands/Generators/CommandGenerator.php
index b844666a794e..8c2ebcfb5d44 100644
--- a/system/Commands/Generators/CommandGenerator.php
+++ b/system/Commands/Generators/CommandGenerator.php
@@ -1,5 +1,7 @@
template = 'command.tpl.php';
$this->classNameLang = 'CLI.generator.className.command';
- $this->execute($params);
+ $this->generateClass($params);
}
/**
diff --git a/system/Commands/Generators/ConfigGenerator.php b/system/Commands/Generators/ConfigGenerator.php
index a83a9671201d..7b1d5f21ff45 100644
--- a/system/Commands/Generators/ConfigGenerator.php
+++ b/system/Commands/Generators/ConfigGenerator.php
@@ -1,5 +1,7 @@
template = 'config.tpl.php';
$this->classNameLang = 'CLI.generator.className.config';
- $this->execute($params);
+ $this->generateClass($params);
}
/**
diff --git a/system/Commands/Generators/ControllerGenerator.php b/system/Commands/Generators/ControllerGenerator.php
index 2cf912b1c7ff..c6f54cac2888 100644
--- a/system/Commands/Generators/ControllerGenerator.php
+++ b/system/Commands/Generators/ControllerGenerator.php
@@ -1,5 +1,7 @@
template = 'controller.tpl.php';
$this->classNameLang = 'CLI.generator.className.controller';
- $this->execute($params);
+ $this->generateClass($params);
}
/**
diff --git a/system/Commands/Generators/EntityGenerator.php b/system/Commands/Generators/EntityGenerator.php
index bd20daf59662..09c350560638 100644
--- a/system/Commands/Generators/EntityGenerator.php
+++ b/system/Commands/Generators/EntityGenerator.php
@@ -1,5 +1,7 @@
template = 'entity.tpl.php';
$this->classNameLang = 'CLI.generator.className.entity';
- $this->execute($params);
+ $this->generateClass($params);
}
}
diff --git a/system/Commands/Generators/FilterGenerator.php b/system/Commands/Generators/FilterGenerator.php
index 620bee5a9ad4..c723da3afab7 100644
--- a/system/Commands/Generators/FilterGenerator.php
+++ b/system/Commands/Generators/FilterGenerator.php
@@ -1,5 +1,7 @@
template = 'filter.tpl.php';
$this->classNameLang = 'CLI.generator.className.filter';
- $this->execute($params);
+ $this->generateClass($params);
}
}
diff --git a/system/Commands/Generators/MigrateCreate.php b/system/Commands/Generators/MigrateCreate.php
deleted file mode 100644
index a2fa6bf76667..000000000000
--- a/system/Commands/Generators/MigrateCreate.php
+++ /dev/null
@@ -1,90 +0,0 @@
-
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Commands\Generators;
-
-use CodeIgniter\CLI\BaseCommand;
-use CodeIgniter\CLI\CLI;
-
-/**
- * Deprecated class for the migration creation command.
- *
- * @deprecated Use make:migration instead.
- *
- * @codeCoverageIgnore
- */
-class MigrateCreate extends BaseCommand
-{
- /**
- * The group the command is lumped under
- * when listing commands.
- *
- * @var string
- */
- protected $group = 'Generators';
-
- /**
- * The Command's name
- *
- * @var string
- */
- protected $name = 'migrate:create';
-
- /**
- * The Command's short description
- *
- * @var string
- */
- protected $description = '[DEPRECATED] Creates a new migration file. Please use "make:migration" instead.';
-
- /**
- * The Command's usage
- *
- * @var string
- */
- protected $usage = 'migrate:create [options]';
-
- /**
- * The Command's arguments.
- *
- * @var array
- */
- protected $arguments = [
- 'name' => 'The migration file name.',
- ];
-
- /**
- * The Command's options.
- *
- * @var array
- */
- protected $options = [
- '--namespace' => 'Set root namespace. Defaults to APP_NAMESPACE',
- '--force' => 'Force overwrite existing files.',
- ];
-
- /**
- * Actually execute a command.
- */
- public function run(array $params)
- {
- // Resolve arguments before passing to make:migration
- $params[0] ??= CLI::getSegment(2);
-
- $params['namespace'] ??= CLI::getOption('namespace') ?? APP_NAMESPACE;
-
- if (array_key_exists('force', $params) || CLI::getOption('force')) {
- $params['force'] = null;
- }
-
- $this->call('make:migration', $params);
- }
-}
diff --git a/system/Commands/Generators/MigrationGenerator.php b/system/Commands/Generators/MigrationGenerator.php
index 52f9e6e53535..b7d7d585e945 100644
--- a/system/Commands/Generators/MigrationGenerator.php
+++ b/system/Commands/Generators/MigrationGenerator.php
@@ -1,5 +1,7 @@
classNameLang = 'CLI.generator.className.migration';
- $this->execute($params);
+ $this->generateClass($params);
}
/**
diff --git a/system/Commands/Generators/ModelGenerator.php b/system/Commands/Generators/ModelGenerator.php
index f4946a9441c9..5450bda79b4c 100644
--- a/system/Commands/Generators/ModelGenerator.php
+++ b/system/Commands/Generators/ModelGenerator.php
@@ -1,5 +1,7 @@
template = 'model.tpl.php';
$this->classNameLang = 'CLI.generator.className.model';
- $this->execute($params);
+ $this->generateClass($params);
}
/**
diff --git a/system/Commands/Generators/ScaffoldGenerator.php b/system/Commands/Generators/ScaffoldGenerator.php
index ef34b92ed572..3b1ef7957c9b 100644
--- a/system/Commands/Generators/ScaffoldGenerator.php
+++ b/system/Commands/Generators/ScaffoldGenerator.php
@@ -1,5 +1,7 @@
template = 'seeder.tpl.php';
$this->classNameLang = 'CLI.generator.className.seeder';
- $this->execute($params);
+ $this->generateClass($params);
}
}
diff --git a/system/Commands/Generators/SessionMigrationGenerator.php b/system/Commands/Generators/SessionMigrationGenerator.php
deleted file mode 100644
index cb7da5892780..000000000000
--- a/system/Commands/Generators/SessionMigrationGenerator.php
+++ /dev/null
@@ -1,113 +0,0 @@
-
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Commands\Generators;
-
-use CodeIgniter\CLI\BaseCommand;
-use CodeIgniter\CLI\CLI;
-use CodeIgniter\CLI\GeneratorTrait;
-use Config\App;
-use Config\Migrations;
-
-/**
- * Generates a migration file for database sessions.
- *
- * @deprecated Use `make:migration --session` instead.
- *
- * @codeCoverageIgnore
- */
-class SessionMigrationGenerator extends BaseCommand
-{
- use GeneratorTrait;
-
- /**
- * The Command's Group
- *
- * @var string
- */
- protected $group = 'Generators';
-
- /**
- * The Command's Name
- *
- * @var string
- */
- protected $name = 'session:migration';
-
- /**
- * The Command's Description
- *
- * @var string
- */
- protected $description = '[DEPRECATED] Generates the migration file for database sessions, Please use "make:migration --session" instead.';
-
- /**
- * The Command's Usage
- *
- * @var string
- */
- protected $usage = 'session:migration [options]';
-
- /**
- * The Command's Options
- *
- * @var array
- */
- protected $options = [
- '-t' => 'Supply a table name.',
- '-g' => 'Database group to use. Default: "default".',
- ];
-
- /**
- * Actually execute a command.
- */
- public function run(array $params)
- {
- $this->component = 'Migration';
- $this->directory = 'Database\Migrations';
- $this->template = 'migration.tpl.php';
-
- $table = 'ci_sessions';
-
- if (array_key_exists('t', $params) || CLI::getOption('t')) {
- $table = $params['t'] ?? CLI::getOption('t');
- }
-
- $params[0] = "_create_{$table}_table";
-
- $this->execute($params);
- }
-
- /**
- * Performs the necessary replacements.
- */
- protected function prepare(string $class): string
- {
- $data = [];
- $data['session'] = true;
- $data['table'] = $this->getOption('t');
- $data['DBGroup'] = $this->getOption('g');
- $data['matchIP'] = config(App::class)->sessionMatchIP ?? false;
-
- $data['table'] = is_string($data['table']) ? $data['table'] : 'ci_sessions';
- $data['DBGroup'] = is_string($data['DBGroup']) ? $data['DBGroup'] : 'default';
-
- return $this->parseTemplate($class, [], [], $data);
- }
-
- /**
- * Change file basename before saving.
- */
- protected function basename(string $filename): string
- {
- return gmdate(config(Migrations::class)->timestampFormat) . basename($filename);
- }
-}
diff --git a/system/Commands/Generators/TestGenerator.php b/system/Commands/Generators/TestGenerator.php
new file mode 100644
index 000000000000..35019c72b826
--- /dev/null
+++ b/system/Commands/Generators/TestGenerator.php
@@ -0,0 +1,192 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Generators;
+
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\CLI\GeneratorTrait;
+
+/**
+ * Generates a skeleton command file.
+ */
+class TestGenerator extends BaseCommand
+{
+ use GeneratorTrait;
+
+ /**
+ * The Command's Group
+ *
+ * @var string
+ */
+ protected $group = 'Generators';
+
+ /**
+ * The Command's Name
+ *
+ * @var string
+ */
+ protected $name = 'make:test';
+
+ /**
+ * The Command's Description
+ *
+ * @var string
+ */
+ protected $description = 'Generates a new test file.';
+
+ /**
+ * The Command's Usage
+ *
+ * @var string
+ */
+ protected $usage = 'make:test [options]';
+
+ /**
+ * The Command's Arguments
+ *
+ * @var array
+ */
+ protected $arguments = [
+ 'name' => 'The test class name.',
+ ];
+
+ /**
+ * The Command's Options
+ *
+ * @var array
+ */
+ protected $options = [
+ '--namespace' => 'Set root namespace. Default: "Tests".',
+ '--force' => 'Force overwrite existing file.',
+ ];
+
+ /**
+ * Actually execute a command.
+ */
+ public function run(array $params)
+ {
+ $this->component = 'Test';
+ $this->template = 'test.tpl.php';
+
+ $this->classNameLang = 'CLI.generator.className.test';
+
+ $autoload = service('autoloader');
+ $autoload->addNamespace('CodeIgniter', TESTPATH . 'system');
+ $autoload->addNamespace('Tests', ROOTPATH . 'tests');
+
+ $this->generateClass($params);
+ }
+
+ /**
+ * Gets the namespace from input or the default namespace.
+ */
+ protected function getNamespace(): string
+ {
+ if ($this->namespace !== null) {
+ return $this->namespace;
+ }
+
+ if ($this->getOption('namespace') !== null) {
+ return trim(
+ str_replace(
+ '/',
+ '\\',
+ $this->getOption('namespace')
+ ),
+ '\\'
+ );
+ }
+
+ $class = $this->normalizeInputClassName();
+ $classPaths = explode('\\', $class);
+
+ $namespaces = service('autoloader')->getNamespace();
+
+ while ($classPaths !== []) {
+ array_pop($classPaths);
+ $namespace = implode('\\', $classPaths);
+
+ foreach (array_keys($namespaces) as $prefix) {
+ if ($prefix === $namespace) {
+ // The input classname is FQCN, and use the namespace.
+ return $namespace;
+ }
+ }
+ }
+
+ return 'Tests';
+ }
+
+ /**
+ * Builds the test file path from the class name.
+ *
+ * @param string $class namespaced classname.
+ */
+ protected function buildPath(string $class): string
+ {
+ $namespace = $this->getNamespace();
+
+ $base = $this->searchTestFilePath($namespace);
+
+ if ($base === null) {
+ CLI::error(
+ lang('CLI.namespaceNotDefined', [$namespace]),
+ 'light_gray',
+ 'red'
+ );
+ CLI::newLine();
+
+ return '';
+ }
+
+ $realpath = realpath($base);
+ $base = ($realpath !== false) ? $realpath : $base;
+
+ $file = $base . DIRECTORY_SEPARATOR
+ . str_replace(
+ '\\',
+ DIRECTORY_SEPARATOR,
+ trim(str_replace($namespace . '\\', '', $class), '\\')
+ ) . '.php';
+
+ return implode(
+ DIRECTORY_SEPARATOR,
+ array_slice(
+ explode(DIRECTORY_SEPARATOR, $file),
+ 0,
+ -1
+ )
+ ) . DIRECTORY_SEPARATOR . $this->basename($file);
+ }
+
+ /**
+ * Returns test file path for the namespace.
+ */
+ private function searchTestFilePath(string $namespace): ?string
+ {
+ $bases = service('autoloader')->getNamespace($namespace);
+
+ $base = null;
+
+ foreach ($bases as $candidate) {
+ if (str_contains($candidate, '/tests/')) {
+ $base = $candidate;
+
+ break;
+ }
+ }
+
+ return $base;
+ }
+}
diff --git a/system/Commands/Generators/ValidationGenerator.php b/system/Commands/Generators/ValidationGenerator.php
index 1b2efb8db18a..d9e3d495a769 100644
--- a/system/Commands/Generators/ValidationGenerator.php
+++ b/system/Commands/Generators/ValidationGenerator.php
@@ -1,5 +1,7 @@
template = 'validation.tpl.php';
$this->classNameLang = 'CLI.generator.className.validation';
- $this->execute($params);
+ $this->generateClass($params);
}
}
diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php
index 72509cdbd9d4..954404f854d4 100644
--- a/system/Commands/Generators/Views/model.tpl.php
+++ b/system/Commands/Generators/Views/model.tpl.php
@@ -18,6 +18,10 @@ class {class} extends Model
protected $allowedFields = [];
protected bool $allowEmptyInserts = false;
+ protected bool $updateOnlyChanged = true;
+
+ protected array $casts = [];
+ protected array $castHandlers = [];
// Dates
protected $useTimestamps = false;
diff --git a/system/Commands/Generators/Views/test.tpl.php b/system/Commands/Generators/Views/test.tpl.php
new file mode 100644
index 000000000000..f67348d079f8
--- /dev/null
+++ b/system/Commands/Generators/Views/test.tpl.php
@@ -0,0 +1,18 @@
+<@php
+
+namespace {namespace};
+
+use CodeIgniter\Test\CIUnitTestCase;
+
+class {class} extends CIUnitTestCase
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+ }
+
+ public function testExample(): void
+ {
+ //
+ }
+}
diff --git a/system/Commands/Help.php b/system/Commands/Help.php
index 338a5c868c72..76913e8a33fc 100644
--- a/system/Commands/Help.php
+++ b/system/Commands/Help.php
@@ -1,5 +1,7 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Translation;
+
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\Helpers\Array\ArrayHelper;
+use Config\App;
+use Locale;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use SplFileInfo;
+
+/**
+ * @see \CodeIgniter\Commands\Translation\LocalizationFinderTest
+ */
+class LocalizationFinder extends BaseCommand
+{
+ protected $group = 'Translation';
+ protected $name = 'lang:find';
+ protected $description = 'Find and save available phrases to translate.';
+ protected $usage = 'lang:find [options]';
+ protected $arguments = [];
+ protected $options = [
+ '--locale' => 'Specify locale (en, ru, etc.) to save files.',
+ '--dir' => 'Directory to search for translations relative to APPPATH.',
+ '--show-new' => 'Show only new translations in table. Does not write to files.',
+ '--verbose' => 'Output detailed information.',
+ ];
+
+ /**
+ * Flag for output detailed information
+ */
+ private bool $verbose = false;
+
+ /**
+ * Flag for showing only translations, without saving
+ */
+ private bool $showNew = false;
+
+ private string $languagePath;
+
+ public function run(array $params)
+ {
+ $this->verbose = array_key_exists('verbose', $params);
+ $this->showNew = array_key_exists('show-new', $params);
+ $optionLocale = $params['locale'] ?? null;
+ $optionDir = $params['dir'] ?? null;
+ $currentLocale = Locale::getDefault();
+ $currentDir = APPPATH;
+ $this->languagePath = $currentDir . 'Language';
+
+ if (ENVIRONMENT === 'testing') {
+ $currentDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR;
+ $this->languagePath = SUPPORTPATH . 'Language';
+ }
+
+ if (is_string($optionLocale)) {
+ if (! in_array($optionLocale, config(App::class)->supportedLocales, true)) {
+ CLI::error(
+ 'Error: "' . $optionLocale . '" is not supported. Supported locales: '
+ . implode(', ', config(App::class)->supportedLocales)
+ );
+
+ return EXIT_USER_INPUT;
+ }
+
+ $currentLocale = $optionLocale;
+ }
+
+ if (is_string($optionDir)) {
+ $tempCurrentDir = realpath($currentDir . $optionDir);
+
+ if ($tempCurrentDir === false) {
+ CLI::error('Error: Directory must be located in "' . $currentDir . '"');
+
+ return EXIT_USER_INPUT;
+ }
+
+ if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) {
+ CLI::error('Error: Directory "' . $this->languagePath . '" restricted to scan.');
+
+ return EXIT_USER_INPUT;
+ }
+
+ $currentDir = $tempCurrentDir;
+ }
+
+ $this->process($currentDir, $currentLocale);
+
+ CLI::write('All operations done!');
+
+ return EXIT_SUCCESS;
+ }
+
+ private function process(string $currentDir, string $currentLocale): void
+ {
+ $tableRows = [];
+ $countNewKeys = 0;
+
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir));
+ $files = iterator_to_array($iterator, true);
+ ksort($files);
+
+ [
+ 'foundLanguageKeys' => $foundLanguageKeys,
+ 'badLanguageKeys' => $badLanguageKeys,
+ 'countFiles' => $countFiles
+ ] = $this->findLanguageKeysInFiles($files);
+
+ ksort($foundLanguageKeys);
+
+ $languageDiff = [];
+ $languageFoundGroups = array_unique(array_keys($foundLanguageKeys));
+
+ foreach ($languageFoundGroups as $langFileName) {
+ $languageStoredKeys = [];
+ $languageFilePath = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php';
+
+ if (is_file($languageFilePath)) {
+ // Load old localization
+ $languageStoredKeys = require $languageFilePath;
+ }
+
+ $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $languageStoredKeys);
+ $countNewKeys += ArrayHelper::recursiveCount($languageDiff);
+
+ if ($this->showNew) {
+ $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows);
+ } else {
+ $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
+
+ if ($languageDiff !== []) {
+ if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) {
+ $this->writeIsVerbose('Lang file ' . $langFileName . ' (error write).', 'red');
+ } else {
+ $this->writeIsVerbose('Lang file "' . $langFileName . '" successful updated!', 'green');
+ }
+ }
+ }
+ }
+
+ if ($this->showNew && $tableRows !== []) {
+ sort($tableRows);
+ CLI::table($tableRows, ['File', 'Key']);
+ }
+
+ if (! $this->showNew && $countNewKeys > 0) {
+ CLI::write('Note: You need to run your linting tool to fix coding standards issues.', 'white', 'red');
+ }
+
+ $this->writeIsVerbose('Files found: ' . $countFiles);
+ $this->writeIsVerbose('New translates found: ' . $countNewKeys);
+ $this->writeIsVerbose('Bad translates found: ' . count($badLanguageKeys));
+
+ if ($this->verbose && $badLanguageKeys !== []) {
+ $tableBadRows = [];
+
+ foreach ($badLanguageKeys as $value) {
+ $tableBadRows[] = [$value[1], $value[0]];
+ }
+
+ ArrayHelper::sortValuesByNatural($tableBadRows, 0);
+
+ CLI::table($tableBadRows, ['Bad Key', 'Filepath']);
+ }
+ }
+
+ /**
+ * @param SplFileInfo|string $file
+ *
+ * @return array
+ */
+ private function findTranslationsInFile($file): array
+ {
+ $foundLanguageKeys = [];
+ $badLanguageKeys = [];
+
+ if (is_string($file) && is_file($file)) {
+ $file = new SplFileInfo($file);
+ }
+
+ $fileContent = file_get_contents($file->getRealPath());
+ preg_match_all('/lang\(\'([._a-z0-9\-]+)\'\)/ui', $fileContent, $matches);
+
+ if ($matches[1] === []) {
+ return compact('foundLanguageKeys', 'badLanguageKeys');
+ }
+
+ foreach ($matches[1] as $phraseKey) {
+ $phraseKeys = explode('.', $phraseKey);
+
+ // Language key not have Filename or Lang key
+ if (count($phraseKeys) < 2) {
+ $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
+
+ continue;
+ }
+
+ $languageFileName = array_shift($phraseKeys);
+ $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '')
+ || ($languageFileName === '' && $phraseKeys[0] !== '')
+ || ($languageFileName === '' && $phraseKeys[0] === '');
+
+ if ($isEmptyNestedArray) {
+ $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
+
+ continue;
+ }
+
+ if (count($phraseKeys) === 1) {
+ $foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey;
+ } else {
+ $childKeys = $this->buildMultiArray($phraseKeys, $phraseKey);
+
+ $foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys);
+ }
+ }
+
+ return compact('foundLanguageKeys', 'badLanguageKeys');
+ }
+
+ private function isIgnoredFile(SplFileInfo $file): bool
+ {
+ if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) {
+ return true;
+ }
+
+ return $file->getExtension() !== 'php';
+ }
+
+ private function templateFile(array $language = []): string
+ {
+ if ($language !== []) {
+ $languageArrayString = var_export($language, true);
+
+ $code = <<replaceArraySyntax($code);
+ }
+
+ return <<<'PHP'
+ $token) {
+ if (is_array($token)) {
+ [$tokenId, $tokenValue] = $token;
+
+ // Replace "array ("
+ if (
+ $tokenId === T_ARRAY
+ && $tokens[$i + 1][0] === T_WHITESPACE
+ && $tokens[$i + 2] === '('
+ ) {
+ $newTokens[$i][1] = '[';
+ $newTokens[$i + 1][1] = '';
+ $newTokens[$i + 2] = '';
+ }
+
+ // Replace indent
+ if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) {
+ $newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}";
+ }
+ } // Replace ")"
+ elseif ($token === ')') {
+ $newTokens[$i] = ']';
+ }
+ }
+
+ $output = '';
+
+ foreach ($newTokens as $token) {
+ $output .= $token[1] ?? $token;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Create multidimensional array from another keys
+ */
+ private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array
+ {
+ $newArray = [];
+ $lastIndex = array_pop($fromKeys);
+ $current = &$newArray;
+
+ foreach ($fromKeys as $value) {
+ $current[$value] = [];
+ $current = &$current[$value];
+ }
+
+ $current[$lastIndex] = $lastArrayValue;
+
+ return $newArray;
+ }
+
+ /**
+ * Convert multi arrays to specific CLI table rows (flat array)
+ */
+ private function arrayToTableRows(string $langFileName, array $array): array
+ {
+ $rows = [];
+
+ foreach ($array as $value) {
+ if (is_array($value)) {
+ $rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value));
+
+ continue;
+ }
+
+ if (is_string($value)) {
+ $rows[] = [$langFileName, $value];
+ }
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Show details in the console if the flag is set
+ */
+ private function writeIsVerbose(string $text = '', ?string $foreground = null, ?string $background = null): void
+ {
+ if ($this->verbose) {
+ CLI::write($text, $foreground, $background);
+ }
+ }
+
+ private function isSubDirectory(string $directory, string $rootDirectory): bool
+ {
+ return 0 === strncmp($directory, $rootDirectory, strlen($directory));
+ }
+
+ /**
+ * @param list $files
+ *
+ * @return array
+ * @phpstan-return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int}
+ */
+ private function findLanguageKeysInFiles(array $files): array
+ {
+ $foundLanguageKeys = [];
+ $badLanguageKeys = [];
+ $countFiles = 0;
+
+ foreach ($files as $file) {
+ if ($this->isIgnoredFile($file)) {
+ continue;
+ }
+
+ $this->writeIsVerbose('File found: ' . mb_substr($file->getRealPath(), mb_strlen(APPPATH)));
+ $countFiles++;
+
+ $findInFile = $this->findTranslationsInFile($file);
+
+ $foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys);
+ $badLanguageKeys = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys);
+ }
+
+ return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles');
+ }
+}
diff --git a/system/Commands/Utilities/ConfigCheck.php b/system/Commands/Utilities/ConfigCheck.php
new file mode 100644
index 000000000000..7d6dc332ca45
--- /dev/null
+++ b/system/Commands/Utilities/ConfigCheck.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities;
+
+use CodeIgniter\Cache\FactoriesCache;
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\Config\BaseConfig;
+use Config\Optimize;
+use Kint\Kint;
+
+/**
+ * Check the Config values.
+ *
+ * @see \CodeIgniter\Commands\Utilities\ConfigCheckTest
+ */
+final class ConfigCheck extends BaseCommand
+{
+ /**
+ * The group the command is lumped under
+ * when listing commands.
+ *
+ * @var string
+ */
+ protected $group = 'CodeIgniter';
+
+ /**
+ * The Command's name
+ *
+ * @var string
+ */
+ protected $name = 'config:check';
+
+ /**
+ * The Command's short description
+ *
+ * @var string
+ */
+ protected $description = 'Check your Config values.';
+
+ /**
+ * The Command's usage
+ *
+ * @var string
+ */
+ protected $usage = 'config:check ';
+
+ /**
+ * The Command's arguments
+ *
+ * @var array
+ */
+ protected $arguments = [
+ 'classname' => 'The config classname to check. Short classname or FQCN.',
+ ];
+
+ /**
+ * The Command's options
+ *
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function run(array $params)
+ {
+ if (! isset($params[0])) {
+ CLI::error('You must specify a Config classname.');
+ CLI::write(' Usage: ' . $this->usage);
+ CLI::write('Example: config:check App');
+ CLI::write(' config:check \'CodeIgniter\Shield\Config\Auth\'');
+
+ return EXIT_ERROR;
+ }
+
+ /** @var class-string $class */
+ $class = $params[0];
+
+ // Load Config cache if it is enabled.
+ $configCacheEnabled = class_exists(Optimize::class)
+ && (new Optimize())->configCacheEnabled;
+ if ($configCacheEnabled) {
+ $factoriesCache = new FactoriesCache();
+ $factoriesCache->load('config');
+ }
+
+ $config = config($class);
+
+ if ($config === null) {
+ CLI::error('No such Config class: ' . $class);
+
+ return EXIT_ERROR;
+ }
+
+ if (defined('KINT_DIR') && Kint::$enabled_mode !== false) {
+ CLI::write($this->getKintD($config));
+ } else {
+ CLI::write(
+ CLI::color($this->getVarDump($config), 'cyan')
+ );
+ }
+
+ CLI::newLine();
+ $state = CLI::color($configCacheEnabled ? 'Enabled' : 'Disabled', 'green');
+ CLI::write('Config Caching: ' . $state);
+
+ return EXIT_SUCCESS;
+ }
+
+ /**
+ * Gets object dump by Kint d()
+ */
+ private function getKintD(object $config): string
+ {
+ ob_start();
+ d($config);
+ $output = ob_get_clean();
+
+ $output = trim($output);
+
+ $lines = explode("\n", $output);
+ array_splice($lines, 0, 3);
+ array_splice($lines, -3);
+
+ return implode("\n", $lines);
+ }
+
+ /**
+ * Gets object dump by var_dump()
+ */
+ private function getVarDump(object $config): string
+ {
+ ob_start();
+ var_dump($config);
+ $output = ob_get_clean();
+
+ return preg_replace(
+ '!.*system/Commands/Utilities/ConfigCheck.php.*\n!u',
+ '',
+ $output
+ );
+ }
+}
diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php
index fd8f68fabdbd..17a08d21d2ce 100644
--- a/system/Commands/Utilities/Environment.php
+++ b/system/Commands/Utilities/Environment.php
@@ -1,5 +1,7 @@
*/
protected $arguments = [
- 'method' => 'The HTTP method. get, post, put, etc.',
+ 'method' => 'The HTTP method. GET, POST, PUT, etc.',
'route' => 'The route (URI path) to check filters.',
];
@@ -76,17 +77,17 @@ public function run(array $params)
if (! isset($params[0], $params[1])) {
CLI::error('You must specify a HTTP verb and a route.');
CLI::write(' Usage: ' . $this->usage);
- CLI::write('Example: filter:check get /');
- CLI::write(' filter:check put products/1');
+ CLI::write('Example: filter:check GET /');
+ CLI::write(' filter:check PUT products/1');
return EXIT_ERROR;
}
- $method = strtolower($params[0]);
+ $method = $params[0];
$route = $params[1];
// Load Routes
- Services::routes()->loadRoutes();
+ service('routes')->loadRoutes();
$filterCollector = new FilterCollector();
@@ -106,6 +107,8 @@ public function run(array $params)
return EXIT_ERROR;
}
+ $filters = $this->addRequiredFilters($filterCollector, $filters);
+
$tbody[] = [
strtoupper($method),
$route,
@@ -124,4 +127,29 @@ public function run(array $params)
return EXIT_SUCCESS;
}
+
+ private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array
+ {
+ $output = [];
+
+ $required = $filterCollector->getRequiredFilters();
+
+ $colored = [];
+
+ foreach ($required['before'] as $filter) {
+ $filter = CLI::color($filter, 'yellow');
+ $colored[] = $filter;
+ }
+ $output['before'] = array_merge($colored, $filters['before']);
+
+ $colored = [];
+
+ foreach ($required['after'] as $filter) {
+ $filter = CLI::color($filter, 'yellow');
+ $colored[] = $filter;
+ }
+ $output['after'] = array_merge($filters['after'], $colored);
+
+ return $output;
+ }
}
diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php
index 71461933c5a0..c16f692cfb37 100644
--- a/system/Commands/Utilities/Namespaces.php
+++ b/system/Commands/Utilities/Namespaces.php
@@ -1,5 +1,7 @@
psr4 as $ns => $paths) {
- if (array_key_exists('r', $params)) {
- $pathOutput = $this->truncate($paths, $maxLength);
- } else {
- $pathOutput = $this->truncate(clean_path($paths), $maxLength);
- }
-
foreach ((array) $paths as $path) {
+ if (array_key_exists('r', $params)) {
+ $pathOutput = $this->truncate($path, $maxLength);
+ } else {
+ $pathOutput = $this->truncate(clean_path($path), $maxLength);
+ }
+
$path = realpath($path) ?: $path;
$tbody[] = [
diff --git a/system/Commands/Utilities/Optimize.php b/system/Commands/Utilities/Optimize.php
new file mode 100644
index 000000000000..fa7612d524a6
--- /dev/null
+++ b/system/Commands/Utilities/Optimize.php
@@ -0,0 +1,149 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities;
+
+use CodeIgniter\Autoloader\FileLocator;
+use CodeIgniter\Autoloader\FileLocatorCached;
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\Publisher\Publisher;
+use RuntimeException;
+
+/**
+ * Optimize for production.
+ */
+final class Optimize extends BaseCommand
+{
+ /**
+ * The group the command is lumped under
+ * when listing commands.
+ *
+ * @var string
+ */
+ protected $group = 'CodeIgniter';
+
+ /**
+ * The Command's name
+ *
+ * @var string
+ */
+ protected $name = 'optimize';
+
+ /**
+ * The Command's short description
+ *
+ * @var string
+ */
+ protected $description = 'Optimize for production.';
+
+ /**
+ * The Command's usage
+ *
+ * @var string
+ */
+ protected $usage = 'optimize';
+
+ /**
+ * {@inheritDoc}
+ */
+ public function run(array $params)
+ {
+ try {
+ $this->enableCaching();
+ $this->clearCache();
+ $this->removeDevPackages();
+ } catch (RuntimeException) {
+ CLI::error('The "spark optimize" failed.');
+
+ return EXIT_ERROR;
+ }
+
+ return EXIT_SUCCESS;
+ }
+
+ private function clearCache(): void
+ {
+ $locator = new FileLocatorCached(new FileLocator(service('autoloader')));
+ $locator->deleteCache();
+ CLI::write('Removed FileLocatorCache.', 'green');
+
+ $cache = WRITEPATH . 'cache/FactoriesCache_config';
+ $this->removeFile($cache);
+ }
+
+ private function removeFile(string $cache): void
+ {
+ if (is_file($cache)) {
+ $result = unlink($cache);
+
+ if ($result) {
+ CLI::write('Removed "' . clean_path($cache) . '".', 'green');
+
+ return;
+ }
+
+ CLI::error('Error in removing file: ' . clean_path($cache));
+
+ throw new RuntimeException(__METHOD__);
+ }
+ }
+
+ private function enableCaching(): void
+ {
+ $publisher = new Publisher(APPPATH, APPPATH);
+
+ $config = APPPATH . 'Config/Optimize.php';
+
+ $result = $publisher->replace(
+ $config,
+ [
+ 'public bool $configCacheEnabled = false;' => 'public bool $configCacheEnabled = true;',
+ 'public bool $locatorCacheEnabled = false;' => 'public bool $locatorCacheEnabled = true;',
+ ]
+ );
+
+ if ($result) {
+ CLI::write(
+ 'Config Caching and FileLocator Caching are enabled in "app/Config/Optimize.php".',
+ 'green'
+ );
+
+ return;
+ }
+
+ CLI::error('Error in updating file: ' . clean_path($config));
+
+ throw new RuntimeException(__METHOD__);
+ }
+
+ private function removeDevPackages(): void
+ {
+ if (! defined('VENDORPATH')) {
+ return;
+ }
+
+ chdir(ROOTPATH);
+ passthru('composer install --no-dev', $status);
+
+ if ($status === 0) {
+ CLI::write('Removed Composer dev packages.', 'green');
+
+ return;
+ }
+
+ CLI::error('Error in removing Composer dev packages.');
+
+ throw new RuntimeException(__METHOD__);
+ }
+}
diff --git a/system/Commands/Utilities/PhpIniCheck.php b/system/Commands/Utilities/PhpIniCheck.php
new file mode 100644
index 000000000000..0426f9078b77
--- /dev/null
+++ b/system/Commands/Utilities/PhpIniCheck.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities;
+
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\Security\CheckPhpIni;
+
+/**
+ * Check php.ini values.
+ */
+final class PhpIniCheck extends BaseCommand
+{
+ /**
+ * The group the command is lumped under
+ * when listing commands.
+ *
+ * @var string
+ */
+ protected $group = 'CodeIgniter';
+
+ /**
+ * The Command's name
+ *
+ * @var string
+ */
+ protected $name = 'phpini:check';
+
+ /**
+ * The Command's short description
+ *
+ * @var string
+ */
+ protected $description = 'Check your php.ini values.';
+
+ /**
+ * The Command's usage
+ *
+ * @var string
+ */
+ protected $usage = 'phpini:check';
+
+ /**
+ * The Command's arguments
+ *
+ * @var array
+ */
+ protected $arguments = [
+ ];
+
+ /**
+ * The Command's options
+ *
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function run(array $params)
+ {
+ CheckPhpIni::run();
+
+ return EXIT_SUCCESS;
+ }
+}
diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php
index 1e4103c114d8..3e19685cba81 100644
--- a/system/Commands/Utilities/Publish.php
+++ b/system/Commands/Utilities/Publish.php
@@ -1,5 +1,7 @@
publish()) {
CLI::write(lang('Publisher.publishSuccess', [
- get_class($publisher),
+ $publisher::class,
count($publisher->getPublished()),
$publisher->getDestination(),
]), 'green');
} else {
CLI::error(lang('Publisher.publishFailure', [
- get_class($publisher),
+ $publisher::class,
$publisher->getDestination(),
]), 'light_gray', 'red');
diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php
index 5d20da07d514..f9575c063973 100644
--- a/system/Commands/Utilities/Routes.php
+++ b/system/Commands/Utilities/Routes.php
@@ -1,5 +1,7 @@
getServer();
$_SERVER['HTTP_HOST'] = $host;
$request->setGlobal('server', $_SERVER);
}
- $collection = Services::routes()->loadRoutes();
+ $collection = service('routes')->loadRoutes();
// Reset HTTP_HOST
if ($host) {
unset($_SERVER['HTTP_HOST']);
}
- $methods = [
- 'get',
- 'head',
- 'post',
- 'patch',
- 'put',
- 'delete',
- 'options',
- 'trace',
- 'connect',
- 'cli',
- ];
+ $methods = Router::HTTP_METHODS;
$tbody = [];
$uriGenerator = new SampleURIGenerator();
@@ -172,8 +163,8 @@ public function run(array $params)
$autoRoutes = $autoRouteCollector->get();
foreach ($autoRoutes as &$routes) {
- // There is no `auto` method, but it is intentional not to get route filters.
- $filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));
+ // There is no `AUTO` method, but it is intentional not to get route filters.
+ $filters = $filterCollector->get('AUTO', $uriGenerator->get($routes[1]));
$routes[] = implode(' ', array_map('class_basename', $filters['before']));
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
@@ -202,5 +193,30 @@ public function run(array $params)
}
CLI::table($tbody, $thead);
+
+ $this->showRequiredFilters();
+ }
+
+ private function showRequiredFilters(): void
+ {
+ $filterCollector = new FilterCollector();
+
+ $required = $filterCollector->getRequiredFilters();
+
+ $filters = [];
+
+ foreach ($required['before'] as $filter) {
+ $filters[] = CLI::color($filter, 'yellow');
+ }
+
+ CLI::write('Required Before Filters: ' . implode(', ', $filters));
+
+ $filters = [];
+
+ foreach ($required['after'] as $filter) {
+ $filters[] = CLI::color($filter, 'yellow');
+ }
+
+ CLI::write(' Required After Filters: ' . implode(', ', $filters));
}
}
diff --git a/system/Commands/Utilities/Routes/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouteCollector.php
index cc45608f21e8..73009b9c9eb8 100644
--- a/system/Commands/Utilities/Routes/AutoRouteCollector.php
+++ b/system/Commands/Utilities/Routes/AutoRouteCollector.php
@@ -1,5 +1,7 @@
namespace = $namespace;
- $this->defaultController = $defaultController;
- $this->defaultMethod = $defaultMethod;
}
/**
diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
index 5a62f8eea8c6..8cbd81fbb02e 100644
--- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
+++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
@@ -1,5 +1,7 @@
- */
- private array $protectedControllers;
-
- /**
- * @var string URI prefix for Module Routing
- */
- private string $prefix;
-
- /**
- * @param string $namespace namespace to search
+ * @param string $namespace namespace to search
+ * @param list $protectedControllers List of controllers in Defined
+ * Routes that should not be accessed via Auto-Routing.
+ * @param string $prefix URI prefix for Module Routing
*/
public function __construct(
- string $namespace,
- string $defaultController,
- string $defaultMethod,
- array $httpMethods,
- array $protectedControllers,
- string $prefix = ''
+ private readonly string $namespace,
+ private readonly string $defaultController,
+ private readonly string $defaultMethod,
+ private readonly array $httpMethods,
+ private readonly array $protectedControllers,
+ private string $prefix = ''
) {
- $this->namespace = $namespace;
- $this->defaultController = $defaultController;
- $this->defaultMethod = $defaultMethod;
- $this->httpMethods = $httpMethods;
- $this->protectedControllers = $protectedControllers;
- $this->prefix = $prefix;
}
/**
diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
index a077633f7011..e08a16ff7a60 100644
--- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
+++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
@@ -1,5 +1,7 @@
- */
- private array $httpMethods;
-
- private bool $translateURIDashes;
+ private readonly bool $translateURIDashes;
+ private readonly bool $translateUriToCamelCase;
/**
- * @param string $namespace the default namespace
+ * @param string $namespace the default namespace
+ * @param list $httpMethods
*/
- public function __construct(string $namespace, array $httpMethods)
- {
- $this->namespace = $namespace;
- $this->httpMethods = $httpMethods;
-
- $config = config(Routing::class);
- $this->translateURIDashes = $config->translateURIDashes;
+ public function __construct(
+ private readonly string $namespace,
+ private readonly array $httpMethods
+ ) {
+ $config = config(Routing::class);
+ $this->translateURIDashes = $config->translateURIDashes;
+ $this->translateUriToCamelCase = $config->translateUriToCamelCase;
}
/**
@@ -65,15 +59,15 @@ public function read(string $class, string $defaultController = 'Home', string $
$classShortname = $reflection->getShortName();
$output = [];
- $classInUri = $this->getUriByClass($classname);
+ $classInUri = $this->convertClassNameToUri($classname);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();
foreach ($this->httpMethods as $httpVerb) {
- if (strpos($methodName, $httpVerb) === 0) {
+ if (str_starts_with($methodName, strtolower($httpVerb))) {
// Remove HTTP verb prefix.
- $methodInUri = $this->getUriByMethod($httpVerb, $methodName);
+ $methodInUri = $this->convertMethodNameToUri($httpVerb, $methodName);
// Check if it is the default method.
if ($methodInUri === $defaultMethod) {
@@ -162,7 +156,7 @@ private function getParameters(ReflectionMethod $method): array
*
* @return string URI path part from the folder(s) and controller
*/
- private function getUriByClass(string $classname): string
+ private function convertClassNameToUri(string $classname): string
{
// remove the namespace
$pattern = '/' . preg_quote($this->namespace, '/') . '/';
@@ -179,25 +173,33 @@ private function getUriByClass(string $classname): string
$classUri = rtrim($classPath, '/');
- if ($this->translateURIDashes) {
- $classUri = str_replace('_', '-', $classUri);
- }
-
- return $classUri;
+ return $this->translateToUri($classUri);
}
/**
* @return string URI path part from the method
*/
- private function getUriByMethod(string $httpVerb, string $methodName): string
+ private function convertMethodNameToUri(string $httpVerb, string $methodName): string
{
$methodUri = lcfirst(substr($methodName, strlen($httpVerb)));
- if ($this->translateURIDashes) {
- $methodUri = str_replace('_', '-', $methodUri);
+ return $this->translateToUri($methodUri);
+ }
+
+ /**
+ * @param string $string classname or method name
+ */
+ private function translateToUri(string $string): string
+ {
+ if ($this->translateUriToCamelCase) {
+ $string = strtolower(
+ preg_replace('/([a-z\d])([A-Z])/', '$1-$2', $string)
+ );
+ } elseif ($this->translateURIDashes) {
+ $string = str_replace('_', '-', $string);
}
- return $methodUri;
+ return $string;
}
/**
diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php
index f11076cf5fff..71de1681b547 100644
--- a/system/Commands/Utilities/Routes/ControllerFinder.php
+++ b/system/Commands/Utilities/Routes/ControllerFinder.php
@@ -1,5 +1,7 @@
namespace = $namespace;
- $this->locator = Services::locator();
+ public function __construct(
+ private readonly string $namespace
+ ) {
+ $this->locator = service('locator');
}
/**
diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php
index 4a37b9560324..c443b669465a 100644
--- a/system/Commands/Utilities/Routes/ControllerMethodReader.php
+++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php
@@ -1,5 +1,7 @@
namespace = $namespace;
}
/**
diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php
index 01d228a41704..002a529b953e 100644
--- a/system/Commands/Utilities/Routes/FilterCollector.php
+++ b/system/Commands/Utilities/Routes/FilterCollector.php
@@ -1,5 +1,7 @@
resetRoutes = $resetRoutes;
+ public function __construct(
+ /**
+ * Whether to reset Defined Routes.
+ *
+ * If set to true, route filters are not found.
+ */
+ private readonly bool $resetRoutes = false
+ ) {
}
/**
- * @param string $method HTTP method
+ * Returns filters for the URI
+ *
+ * @param string $method HTTP verb like `GET`,`POST` or `CLI`.
* @param string $uri URI path to find filters for
*
* @return array{before: list, after: list} array of filter alias or classname
*/
public function get(string $method, string $uri): array
{
- if ($method === 'cli') {
+ if ($method === strtolower($method)) {
+ @trigger_error(
+ 'Passing lowercase HTTP method "' . $method . '" is deprecated.'
+ . ' Use uppercase HTTP method like "' . strtoupper($method) . '".',
+ E_USER_DEPRECATED
+ );
+ }
+
+ /**
+ * @deprecated 4.5.0
+ * @TODO Remove this in the future.
+ */
+ $method = strtoupper($method);
+
+ if ($method === 'CLI') {
return [
'before' => [],
'after' => [],
@@ -62,9 +79,27 @@ public function get(string $method, string $uri): array
return $finder->find($uri);
}
+ /**
+ * Returns Required Filters
+ *
+ * @return array{before: list, after: list} array of filter alias or classname
+ */
+ public function getRequiredFilters(): array
+ {
+ $request = Services::incomingrequest(null, false);
+ $request->setMethod(Method::GET);
+
+ $router = $this->createRouter($request);
+ $filters = $this->createFilters($request);
+
+ $finder = new FilterFinder($router, $filters);
+
+ return $finder->getRequiredFilters();
+ }
+
private function createRouter(Request $request): Router
{
- $routes = Services::routes();
+ $routes = service('routes');
if ($this->resetRoutes) {
$routes->resetRoutes();
@@ -77,6 +112,6 @@ private function createFilters(Request $request): Filters
{
$config = config(FiltersConfig::class);
- return new Filters($config, $request, Services::response());
+ return new Filters($config, $request, service('response'));
}
}
diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php
index 2e5da617e795..82ea26979517 100644
--- a/system/Commands/Utilities/Routes/FilterFinder.php
+++ b/system/Commands/Utilities/Routes/FilterFinder.php
@@ -1,5 +1,7 @@
router = $router ?? Services::router();
- $this->filters = $filters ?? Services::filters();
+ $this->router = $router ?? service('router');
+ $this->filters = $filters ?? service('filters');
}
private function getRouteFilters(string $uri): array
{
$this->router->handle($uri);
- $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false;
- if (! $multipleFiltersEnabled) {
- $filter = $this->router->getFilter();
-
- return $filter === null ? [] : [$filter];
- }
-
return $this->router->getFilters();
}
@@ -60,22 +54,45 @@ public function find(string $uri): array
// Add route filters
try {
$routeFilters = $this->getRouteFilters($uri);
+
$this->filters->enableFilters($routeFilters, 'before');
+
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+ if (! $oldFilterOrder) {
+ $routeFilters = array_reverse($routeFilters);
+ }
+
$this->filters->enableFilters($routeFilters, 'after');
$this->filters->initialize($uri);
return $this->filters->getFilters();
- } catch (RedirectException $e) {
+ } catch (RedirectException) {
return [
'before' => [],
'after' => [],
];
- } catch (PageNotFoundException $e) {
+ } catch (PageNotFoundException) {
return [
'before' => [''],
'after' => [''],
];
}
}
+
+ /**
+ * Returns Required Filters
+ *
+ * @return array{before: list, after:list}
+ */
+ public function getRequiredFilters(): array
+ {
+ [$requiredBefore] = $this->filters->getRequiredFilters('before');
+ [$requiredAfter] = $this->filters->getRequiredFilters('after');
+
+ return [
+ 'before' => $requiredBefore,
+ 'after' => $requiredAfter,
+ ];
+ }
}
diff --git a/system/Commands/Utilities/Routes/SampleURIGenerator.php b/system/Commands/Utilities/Routes/SampleURIGenerator.php
index 43d1934438bb..45eb2f93201b 100644
--- a/system/Commands/Utilities/Routes/SampleURIGenerator.php
+++ b/system/Commands/Utilities/Routes/SampleURIGenerator.php
@@ -1,5 +1,7 @@
routes = $routes ?? Services::routes();
+ $this->routes = $routes ?? service('routes');
}
/**
@@ -52,7 +53,7 @@ public function get(string $routeKey): string
{
$sampleUri = $routeKey;
- if (strpos($routeKey, '{locale}') !== false) {
+ if (str_contains($routeKey, '{locale}')) {
$sampleUri = str_replace(
'{locale}',
config(App::class)->defaultLocale,
diff --git a/system/Common.php b/system/Common.php
index 2ad287fb26ea..f96e9f100f00 100644
--- a/system/Common.php
+++ b/system/Common.php
@@ -1,5 +1,7 @@
'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH)),
+ str_starts_with($path, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH)),
+ str_starts_with($path, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH)),
+ defined('VENDORPATH') && str_starts_with($path, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH)),
+ str_starts_with($path, ROOTPATH) => 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH)),
+ default => $path,
+ };
}
}
@@ -212,6 +203,10 @@ function command(string $command)
*/
function config(string $name, bool $getShared = true)
{
+ if ($getShared) {
+ return Factories::get('config', $name);
+ }
+
return Factories::config($name, ['getShared' => $getShared]);
}
}
@@ -242,7 +237,7 @@ function cookie(string $name, string $value = '', array $options = []): Cookie
function cookies(array $cookies = [], bool $getGlobal = true): CookieStore
{
if ($getGlobal) {
- return Services::response()->getCookieStore();
+ return service('response')->getCookieStore();
}
return new CookieStore($cookies);
@@ -257,7 +252,7 @@ function cookies(array $cookies = [], bool $getGlobal = true): CookieStore
*/
function csrf_token(): string
{
- return Services::security()->getTokenName();
+ return service('security')->getTokenName();
}
}
@@ -269,7 +264,7 @@ function csrf_token(): string
*/
function csrf_header(): string
{
- return Services::security()->getHeaderName();
+ return service('security')->getHeaderName();
}
}
@@ -281,7 +276,7 @@ function csrf_header(): string
*/
function csrf_hash(): string
{
- return Services::security()->getHash();
+ return service('security')->getHash();
}
}
@@ -315,7 +310,7 @@ function csrf_meta(?string $id = null): string
*/
function csp_style_nonce(): string
{
- $csp = Services::csp();
+ $csp = service('csp');
if (! $csp->enabled()) {
return '';
@@ -331,7 +326,7 @@ function csp_style_nonce(): string
*/
function csp_script_nonce(): string
{
- $csp = Services::csp();
+ $csp = service('csp');
if (! $csp->enabled()) {
return '';
@@ -387,21 +382,13 @@ function env(string $key, $default = null)
}
// Handle any boolean values
- switch (strtolower($value)) {
- case 'true':
- return true;
-
- case 'false':
- return false;
-
- case 'empty':
- return '';
-
- case 'null':
- return null;
- }
-
- return $value;
+ return match (strtolower($value)) {
+ 'true' => true,
+ 'false' => false,
+ 'empty' => '',
+ 'null' => null,
+ default => $value,
+ };
}
}
@@ -484,13 +471,13 @@ function force_https(
?RequestInterface $request = null,
?ResponseInterface $response = null
): void {
- $request ??= Services::request();
+ $request ??= service('request');
if (! $request instanceof IncomingRequest) {
return;
}
- $response ??= Services::response();
+ $response ??= service('response');
if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure()))
|| $request->getServer('HTTPS') === 'test'
@@ -501,7 +488,7 @@ function force_https(
// If the session status is active, we should regenerate
// the session ID for safety sake.
if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) {
- Services::session()->regenerate(); // @codeCoverageIgnore
+ service('session')->regenerate(); // @codeCoverageIgnore
}
$uri = $request->getUri()->withScheme('https');
@@ -580,7 +567,7 @@ function helper($filenames): void
{
static $loaded = [];
- $loader = Services::locator();
+ $loader = service('locator');
if (! is_array($filenames)) {
$filenames = [$filenames];
@@ -596,7 +583,7 @@ function helper($filenames): void
$appHelper = null;
$localIncludes = [];
- if (strpos($filename, '_helper') === false) {
+ if (! str_contains($filename, '_helper')) {
$filename .= '_helper';
}
@@ -607,7 +594,7 @@ function helper($filenames): void
// If the file is namespaced, we'll just grab that
// file and not search for any others
- if (strpos($filename, '\\') !== false) {
+ if (str_contains($filename, '\\')) {
$path = $loader->locateFile($filename, 'Helpers');
if (empty($path)) {
@@ -621,9 +608,9 @@ function helper($filenames): void
$paths = $loader->search('Helpers/' . $filename);
foreach ($paths as $path) {
- if (strpos($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
+ if (str_starts_with($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
$appHelper = $path;
- } elseif (strpos($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
+ } elseif (str_starts_with($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
$systemHelper = $path;
} else {
$localIncludes[] = $path;
@@ -745,7 +732,7 @@ function is_windows(?bool $mock = null): bool
*/
function lang(string $line, array $args = [], ?string $locale = null)
{
- $language = Services::language();
+ $language = service('language');
// Get active locale
$activeLocale = $language->getLocale();
@@ -754,14 +741,14 @@ function lang(string $line, array $args = [], ?string $locale = null)
$language->setLocale($locale);
}
- $line = $language->getLine($line, $args);
+ $lines = $language->getLine($line, $args);
if ($locale && $locale !== $activeLocale) {
// Reset to active locale
$language->setLocale($activeLocale);
}
- return $line;
+ return $lines;
}
}
@@ -780,7 +767,7 @@ function lang(string $line, array $args = [], ?string $locale = null)
* - info
* - debug
*
- * @return bool
+ * @return void
*/
function log_message(string $level, string $message, array $context = [])
{
@@ -790,10 +777,12 @@ function log_message(string $level, string $message, array $context = [])
if (ENVIRONMENT === 'testing') {
$logger = new TestLogger(new Logger());
- return $logger->log($level, $message, $context);
+ $logger->log($level, $message, $context);
+
+ return;
}
- return Services::logger(true)->log($level, $message, $context); // @codeCoverageIgnore
+ service('logger')->log($level, $message, $context); // @codeCoverageIgnore
}
}
@@ -832,7 +821,7 @@ function old(string $key, $default = null, $escape = 'html')
session(); // @codeCoverageIgnore
}
- $request = Services::request();
+ $request = service('request');
$value = $request->getOldInput($key);
@@ -858,7 +847,7 @@ function old(string $key, $default = null, $escape = 'html')
*/
function redirect(?string $route = null): RedirectResponse
{
- $response = Services::redirectresponse(null, true);
+ $response = service('redirectresponse');
if ($route !== null) {
return $response->route($route);
@@ -930,7 +919,7 @@ function remove_invisible_characters(string $str, bool $urlEncoded = true): stri
*/
function request()
{
- return Services::request();
+ return service('request');
}
}
@@ -940,7 +929,7 @@ function request()
*/
function response(): ResponseInterface
{
- return Services::response();
+ return service('response');
}
}
@@ -961,7 +950,7 @@ function response(): ResponseInterface
*/
function route_to(string $method, ...$params)
{
- return Services::routes()->reverseRoute($method, ...$params);
+ return service('routes')->reverseRoute($method, ...$params);
}
}
@@ -979,7 +968,7 @@ function route_to(string $method, ...$params)
*/
function session(?string $val = null)
{
- $session = Services::session();
+ $session = service('session');
// Returning a single item?
if (is_string($val)) {
@@ -1005,6 +994,10 @@ function session(?string $val = null)
*/
function service(string $name, ...$params): ?object
{
+ if ($params === []) {
+ return Services::get($name);
+ }
+
return Services::$name(...$params);
}
}
@@ -1133,7 +1126,7 @@ function stringify_attributes($attributes, bool $js = false): string
*/
function timer(?string $name = null, ?callable $callable = null)
{
- $timer = Services::timer();
+ $timer = service('timer');
if ($name === null) {
return $timer;
@@ -1165,7 +1158,7 @@ function timer(?string $name = null, ?callable $callable = null)
*/
function view(string $name, array $data = [], array $options = []): string
{
- $renderer = Services::renderer();
+ $renderer = service('renderer');
$config = config(View::class);
$saveData = $config->saveData;
@@ -1190,7 +1183,7 @@ function view(string $name, array $data = [], array $options = []): string
*/
function view_cell(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string
{
- return Services::viewcell()
+ return service('viewcell')
->render($library, $params, $ttl, $cacheName);
}
}
@@ -1213,7 +1206,7 @@ function view_cell(string $library, $params = null, int $ttl = 0, ?string $cache
*/
function class_basename($class)
{
- $class = is_object($class) ? get_class($class) : $class;
+ $class = is_object($class) ? $class::class : $class;
return basename(str_replace('\\', '/', $class));
}
@@ -1232,7 +1225,7 @@ function class_basename($class)
function class_uses_recursive($class)
{
if (is_object($class)) {
- $class = get_class($class);
+ $class = $class::class;
}
$results = [];
diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php
index b95bdf54c4ae..661247dab532 100644
--- a/system/ComposerScripts.php
+++ b/system/ComposerScripts.php
@@ -1,5 +1,7 @@
[
'license' => __DIR__ . '/../vendor/psr/log/LICENSE',
- 'from' => __DIR__ . '/../vendor/psr/log/Psr/Log/',
+ 'from' => __DIR__ . '/../vendor/psr/log/src/',
'to' => __DIR__ . '/ThirdParty/PSR/Log/',
],
];
@@ -71,7 +73,7 @@ public static function postUpdate()
foreach (self::$dependencies as $key => $dependency) {
// Kint may be removed.
- if (! is_dir($dependency['from']) && strpos($key, 'kint') === 0) {
+ if (! is_dir($dependency['from']) && str_starts_with($key, 'kint')) {
continue;
}
@@ -84,7 +86,6 @@ public static function postUpdate()
}
self::copyKintInitFiles();
- self::recursiveDelete(self::$dependencies['psr-log']['to'] . 'Test/');
}
/**
diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php
index 33c977cc0bb6..0a99cdb0bb28 100644
--- a/system/Config/AutoloadConfig.php
+++ b/system/Config/AutoloadConfig.php
@@ -1,5 +1,7 @@
SYSTEMPATH,
- 'App' => APPPATH, // To ensure filters, etc still found,
+ 'Config' => APPPATH . 'Config',
];
/**
@@ -103,7 +105,7 @@ class AutoloadConfig
* searched for within one or more directories as they would if they
* were being autoloaded through a namespace.
*
- * @var array
+ * @var array
*/
protected $coreClassmap = [
AbstractLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php',
diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php
index 5dd6cde2ad02..8a82cbf7ce64 100644
--- a/system/Config/BaseConfig.php
+++ b/system/Config/BaseConfig.php
@@ -13,7 +13,6 @@
use Config\Encryption;
use Config\Modules;
-use Config\Services;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
@@ -120,10 +119,10 @@ public function __construct()
$this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
if ($this instanceof Encryption && $property === 'key') {
- if (strpos($this->{$property}, 'hex2bin:') === 0) {
+ if (str_starts_with($this->{$property}, 'hex2bin:')) {
// Handle hex2bin prefix
$this->{$property} = hex2bin(substr($this->{$property}, 8));
- } elseif (strpos($this->{$property}, 'base64:') === 0) {
+ } elseif (str_starts_with($this->{$property}, 'base64:')) {
// Handle base64 prefix
$this->{$property} = base64_decode(substr($this->{$property}, 7), true);
}
@@ -164,6 +163,9 @@ protected function initEnvValue(&$property, string $name, string $prefix, string
$value = (float) $value;
}
+ // If the default value of the property is `null` and the type is not
+ // `string`, TypeError will happen.
+ // So cannot set `declare(strict_types=1)` in this file.
$property = $value;
}
}
@@ -228,11 +230,16 @@ protected function registerProperties()
}
if (! static::$didDiscovery) {
- $locator = Services::locator();
+ $locator = service('locator');
$registrarsFiles = $locator->search('Config/Registrar.php');
foreach ($registrarsFiles as $file) {
- $className = $locator->getClassname($file);
+ $className = $locator->findQualifiedNameFromPath($file);
+
+ if ($className === false) {
+ continue;
+ }
+
static::$registrars[] = new $className();
}
diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php
index 835145eee460..2f8df2a35420 100644
--- a/system/Config/BaseService.php
+++ b/system/Config/BaseService.php
@@ -1,5 +1,7 @@
[key => instance]
*/
protected static $instances = [];
+ /**
+ * Factory method list.
+ *
+ * @var array [key => callable]
+ */
+ protected static array $factories = [];
+
/**
* Mock objects for testing which are returned if exist.
*
- * @var array
+ * @var array [key => instance]
*/
protected static $mocks = [];
@@ -164,6 +177,8 @@ class BaseService
* A cache of other service classes we've found.
*
* @var array
+ *
+ * @deprecated 4.5.0 No longer used.
*/
protected static $services = [];
@@ -174,6 +189,42 @@ class BaseService
*/
private static array $serviceNames = [];
+ /**
+ * Simple method to get an entry fast.
+ *
+ * @param string $key Identifier of the entry to look for.
+ *
+ * @return object|null Entry.
+ */
+ public static function get(string $key): ?object
+ {
+ return static::$instances[$key] ?? static::__callStatic($key, []);
+ }
+
+ /**
+ * Sets an entry.
+ *
+ * @param string $key Identifier of the entry.
+ */
+ public static function set(string $key, object $value): void
+ {
+ if (isset(static::$instances[$key])) {
+ throw new InvalidArgumentException('The entry for "' . $key . '" is already set.');
+ }
+
+ static::$instances[$key] = $value;
+ }
+
+ /**
+ * Overrides an existing entry.
+ *
+ * @param string $key Identifier of the entry.
+ */
+ public static function override(string $key, object $value): void
+ {
+ static::$instances[$key] = $value;
+ }
+
/**
* Returns a shared instance of any of the class' services.
*
@@ -226,13 +277,20 @@ public static function autoloader(bool $getShared = true)
* within namespaced folders, as well as convenience methods for
* loading 'helpers', and 'libraries'.
*
- * @return FileLocator
+ * @return FileLocatorInterface
*/
public static function locator(bool $getShared = true)
{
if ($getShared) {
if (empty(static::$instances['locator'])) {
- static::$instances['locator'] = new FileLocator(static::autoloader());
+ $cacheEnabled = class_exists(Optimize::class)
+ && (new Optimize())->locatorCacheEnabled;
+
+ if ($cacheEnabled) {
+ static::$instances['locator'] = new FileLocatorCached(new FileLocator(static::autoloader()));
+ } else {
+ static::$instances['locator'] = new FileLocator(static::autoloader());
+ }
}
return static::$mocks['locator'] ?? static::$instances['locator'];
@@ -249,6 +307,10 @@ public static function locator(bool $getShared = true)
*/
public static function __callStatic(string $name, array $arguments)
{
+ if (isset(static::$factories[$name])) {
+ return static::$factories[$name](...$arguments);
+ }
+
$service = static::serviceExists($name);
if ($service === null) {
@@ -265,11 +327,14 @@ public static function __callStatic(string $name, array $arguments)
public static function serviceExists(string $name): ?string
{
static::buildServicesCache();
+
$services = array_merge(self::$serviceNames, [Services::class]);
$name = strtolower($name);
foreach ($services as $service) {
if (method_exists($service, $name)) {
+ static::$factories[$name] = [$service, $name];
+
return $service;
}
}
@@ -286,6 +351,7 @@ public static function reset(bool $initAutoloader = true)
{
static::$mocks = [];
static::$instances = [];
+ static::$factories = [];
if ($initAutoloader) {
static::autoloader()->initialize(new Autoload(), new Modules());
@@ -312,75 +378,27 @@ public static function resetSingle(string $name)
*/
public static function injectMock(string $name, $mock)
{
+ static::$instances[$name] = $mock;
static::$mocks[strtolower($name)] = $mock;
}
- /**
- * Will scan all psr4 namespaces registered with system to look
- * for new Config\Services files. Caches a copy of each one, then
- * looks for the service method in each, returning an instance of
- * the service, if available.
- *
- * @return object|null
- *
- * @deprecated
- *
- * @codeCoverageIgnore
- */
- protected static function discoverServices(string $name, array $arguments)
+ protected static function buildServicesCache(): void
{
if (! static::$discovered) {
if ((new Modules())->shouldDiscover('services')) {
$locator = static::locator();
$files = $locator->search('Config/Services');
- if (empty($files)) {
- // no files at all found - this would be really, really bad
- return null;
- }
-
// Get instances of all service classes and cache them locally.
foreach ($files as $file) {
- $classname = $locator->getClassname($file);
+ $classname = $locator->findQualifiedNameFromPath($file);
- if ($classname !== Services::class) {
- static::$services[] = new $classname();
+ if ($classname === false) {
+ continue;
}
- }
- }
-
- static::$discovered = true;
- }
-
- if (! static::$services) {
- // we found stuff, but no services - this would be really bad
- return null;
- }
-
- // Try to find the desired service method
- foreach (static::$services as $class) {
- if (method_exists($class, $name)) {
- return $class::$name(...$arguments);
- }
- }
-
- return null;
- }
-
- protected static function buildServicesCache(): void
- {
- if (! static::$discovered) {
- if ((new Modules())->shouldDiscover('services')) {
- $locator = static::locator();
- $files = $locator->search('Config/Services');
-
- // Get instances of all service classes and cache them locally.
- foreach ($files as $file) {
- $classname = $locator->getClassname($file);
if ($classname !== Services::class) {
self::$serviceNames[] = $classname;
- static::$services[] = new $classname();
}
}
}
diff --git a/system/Config/Config.php b/system/Config/Config.php
deleted file mode 100644
index 90a6b7cadef5..000000000000
--- a/system/Config/Config.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Config;
-
-/**
- * @deprecated Use CodeIgniter\Config\Factories::config()
- * @see \CodeIgniter\Config\ConfigTest
- */
-class Config
-{
- /**
- * Create new configuration instances or return
- * a shared instance
- *
- * @param string $name Configuration name
- * @param bool $getShared Use shared instance
- *
- * @return object|null
- */
- public static function get(string $name, bool $getShared = true)
- {
- return Factories::config($name, ['getShared' => $getShared]);
- }
-
- /**
- * Helper method for injecting mock instances while testing.
- *
- * @param object $instance
- *
- * @return void
- */
- public static function injectMock(string $name, $instance)
- {
- Factories::injectMock('config', $name, $instance);
- }
-
- /**
- * Resets the static arrays
- *
- * @return void
- */
- public static function reset()
- {
- Factories::reset('config');
- }
-}
diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php
index f8e757e9eab5..db7152fd09d5 100644
--- a/system/Config/DotEnv.php
+++ b/system/Config/DotEnv.php
@@ -1,5 +1,7 @@
normaliseVariable($line);
$vars[$name] = $value;
$this->setVariable($name, $value);
@@ -112,7 +114,7 @@ protected function setVariable(string $name, string $value = '')
public function normaliseVariable(string $name, string $value = ''): array
{
// Split our compound string into its parts.
- if (strpos($name, '=') !== false) {
+ if (str_contains($name, '=')) {
[$name, $value] = explode('=', $name, 2);
}
@@ -192,7 +194,7 @@ protected function sanitizeValue(string $value): string
*/
protected function resolveNestedVariables(string $value): string
{
- if (strpos($value, '$') !== false) {
+ if (str_contains($value, '$')) {
$value = preg_replace_callback(
'/\${([a-zA-Z0-9_\.]+)}/',
function ($matchedPatterns) {
diff --git a/system/Config/Factories.php b/system/Config/Factories.php
index c86bfd95cc5d..f8102118bc1d 100644
--- a/system/Config/Factories.php
+++ b/system/Config/Factories.php
@@ -1,5 +1,7 @@
>
*/
- protected static $options = [];
+ private static $options = [];
/**
* Explicit options for the Config
@@ -64,7 +65,7 @@ class Factories
*
* @var array>
*/
- protected static $aliases = [];
+ private static $aliases = [];
/**
* Store for instances of any component that
@@ -77,7 +78,7 @@ class Factories
*
* @var array>
*/
- protected static $instances = [];
+ private static $instances = [];
/**
* Whether the component instances are updated?
@@ -86,7 +87,7 @@ class Factories
*
* @internal For caching only
*/
- protected static $updated = [];
+ private static $updated = [];
/**
* Define the class to load. You can *override* the concrete class.
@@ -139,8 +140,8 @@ public static function __callStatic(string $component, array $arguments)
$options = array_merge(self::getOptions($component), $options);
if (! $options['getShared']) {
- if (isset(self::$aliases[$component][$alias])) {
- $class = self::$aliases[$component][$alias];
+ if (isset(self::$aliases[$options['component']][$alias])) {
+ $class = self::$aliases[$options['component']][$alias];
return new $class(...$arguments);
}
@@ -171,6 +172,20 @@ public static function __callStatic(string $component, array $arguments)
return self::$instances[$options['component']][$class];
}
+ /**
+ * Simple method to get the shared instance fast.
+ */
+ public static function get(string $component, string $alias): ?object
+ {
+ if (isset(self::$aliases[$component][$alias])) {
+ $class = self::$aliases[$component][$alias];
+
+ return self::$instances[$component][$class];
+ }
+
+ return self::__callStatic($component, [$alias]);
+ }
+
/**
* Gets the defined instance. If not exists, creates new one.
*
@@ -249,7 +264,7 @@ private static function isConfig(string $component): bool
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
- protected static function locateClass(array $options, string $alias): ?string
+ private static function locateClass(array $options, string $alias): ?string
{
// Check for low-hanging fruit
if (
@@ -282,7 +297,7 @@ class_exists($alias, false)
}
// Have to do this the hard way...
- $locator = Services::locator();
+ $locator = service('locator');
// Check if the class alias was namespaced
if (self::isNamespaced($alias)) {
@@ -299,9 +314,9 @@ class_exists($alias, false)
// Check all files for a valid class
foreach ($files as $file) {
- $class = $locator->getClassname($file);
+ $class = $locator->findQualifiedNameFromPath($file);
- if ($class && self::verifyInstanceOf($options, $class)) {
+ if ($class !== false && self::verifyInstanceOf($options, $class)) {
return $class;
}
}
@@ -316,7 +331,7 @@ class_exists($alias, false)
*/
private static function isNamespaced(string $alias): bool
{
- return strpos($alias, '\\') !== false;
+ return str_contains($alias, '\\');
}
/**
@@ -325,7 +340,7 @@ private static function isNamespaced(string $alias): bool
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
- protected static function verifyPreferApp(array $options, string $alias): bool
+ private static function verifyPreferApp(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['preferApp']) {
@@ -334,10 +349,10 @@ protected static function verifyPreferApp(array $options, string $alias): bool
// Special case for Config since its App namespace is actually \Config
if (self::isConfig($options['component'])) {
- return strpos($alias, 'Config') === 0;
+ return str_starts_with($alias, 'Config');
}
- return strpos($alias, APP_NAMESPACE) === 0;
+ return str_starts_with($alias, APP_NAMESPACE);
}
/**
@@ -346,7 +361,7 @@ protected static function verifyPreferApp(array $options, string $alias): bool
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
- protected static function verifyInstanceOf(array $options, string $alias): bool
+ private static function verifyInstanceOf(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['instanceOf']) {
@@ -461,7 +476,7 @@ public static function injectMock(string $component, string $alias, object $inst
// Force a configuration to exist for this component
self::getOptions($component);
- $class = get_class($instance);
+ $class = $instance::class;
self::$instances[$component][$class] = $instance;
self::$aliases[$component][$alias] = $class;
diff --git a/system/Config/Factory.php b/system/Config/Factory.php
index 5889bf4d7336..b25267791beb 100644
--- a/system/Config/Factory.php
+++ b/system/Config/Factory.php
@@ -1,5 +1,7 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Config;
+
+use CodeIgniter\Filters\Cors;
+use CodeIgniter\Filters\CSRF;
+use CodeIgniter\Filters\DebugToolbar;
+use CodeIgniter\Filters\ForceHTTPS;
+use CodeIgniter\Filters\Honeypot;
+use CodeIgniter\Filters\InvalidChars;
+use CodeIgniter\Filters\PageCache;
+use CodeIgniter\Filters\PerformanceMetrics;
+use CodeIgniter\Filters\SecureHeaders;
+
+/**
+ * Filters configuration
+ */
+class Filters extends BaseConfig
+{
+ /**
+ * Configures aliases for Filter classes to
+ * make reading things nicer and simpler.
+ *
+ * @var array>
+ *
+ * [filter_name => classname]
+ * or [filter_name => [classname1, classname2, ...]]
+ */
+ public array $aliases = [
+ 'csrf' => CSRF::class,
+ 'toolbar' => DebugToolbar::class,
+ 'honeypot' => Honeypot::class,
+ 'invalidchars' => InvalidChars::class,
+ 'secureheaders' => SecureHeaders::class,
+ 'cors' => Cors::class,
+ 'forcehttps' => ForceHTTPS::class,
+ 'pagecache' => PageCache::class,
+ 'performance' => PerformanceMetrics::class,
+ ];
+
+ /**
+ * List of special required filters.
+ *
+ * The filters listed here are special. They are applied before and after
+ * other kinds of filters, and always applied even if a route does not exist.
+ *
+ * Filters set by default provide framework functionality. If removed,
+ * those functions will no longer work.
+ *
+ * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
+ *
+ * @var array{before: list, after: list}
+ */
+ public array $required = [
+ 'before' => [
+ 'forcehttps', // Force Global Secure Requests
+ 'pagecache', // Web Page Caching
+ ],
+ 'after' => [
+ 'pagecache', // Web Page Caching
+ 'performance', // Performance Metrics
+ 'toolbar', // Debug Toolbar
+ ],
+ ];
+
+ /**
+ * List of filter aliases that are always
+ * applied before and after every request.
+ *
+ * @var array>>|array>
+ */
+ public array $globals = [
+ 'before' => [
+ // 'honeypot',
+ // 'csrf',
+ // 'invalidchars',
+ ],
+ 'after' => [
+ // 'honeypot',
+ // 'secureheaders',
+ ],
+ ];
+
+ /**
+ * List of filter aliases that works on a
+ * particular HTTP method (GET, POST, etc.).
+ *
+ * Example:
+ * 'POST' => ['foo', 'bar']
+ *
+ * If you use this, you should disable auto-routing because auto-routing
+ * permits any HTTP method to access a controller. Accessing the controller
+ * with a method you don't expect could bypass the filter.
+ */
+ public array $methods = [];
+
+ /**
+ * List of filter aliases that should run on any
+ * before or after URI patterns.
+ *
+ * Example:
+ * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
+ */
+ public array $filters = [];
+}
diff --git a/system/Config/ForeignCharacters.php b/system/Config/ForeignCharacters.php
index a8569f5419c5..2663a53a82ed 100644
--- a/system/Config/ForeignCharacters.php
+++ b/system/Config/ForeignCharacters.php
@@ -1,5 +1,7 @@
*/
public array $moduleRoutes = [];
+
+ /**
+ * For Auto Routing (Improved).
+ * Whether to translate dashes in URIs for controller/method to CamelCase.
+ * E.g., blog-controller -> BlogController
+ *
+ * If you enable this, $translateURIDashes is ignored.
+ *
+ * Default: false
+ */
+ public bool $translateUriToCamelCase = false;
}
diff --git a/system/Config/Services.php b/system/Config/Services.php
index 738809b44595..422799e0b87c 100644
--- a/system/Config/Services.php
+++ b/system/Config/Services.php
@@ -1,5 +1,7 @@
withFile($path)->rotate(90)->save();
+ * of the handler. Used like service('image')->withFile($path)->rotate(90)->save();
*
* @return BaseHandler
*/
@@ -374,8 +376,8 @@ public static function language(?string $locale = null, bool $getShared = true)
return static::getSharedInstance('language', $locale)->setLocale($locale);
}
- if (AppServices::request() instanceof IncomingRequest) {
- $requestLocale = AppServices::request()->getLocale();
+ if (AppServices::get('request') instanceof IncomingRequest) {
+ $requestLocale = AppServices::get('request')->getLocale();
} else {
$requestLocale = Locale::getDefault();
}
@@ -430,7 +432,7 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh
return static::getSharedInstance('negotiator', $request);
}
- $request ??= AppServices::request();
+ $request ??= AppServices::get('request');
return new Negotiate($request);
}
@@ -447,7 +449,7 @@ public static function responsecache(?Cache $config = null, ?CacheInterface $cac
}
$config ??= config(Cache::class);
- $cache ??= AppServices::cache();
+ $cache ??= AppServices::get('cache');
return new ResponseCache($config, $cache);
}
@@ -483,7 +485,7 @@ public static function parser(?string $viewPath = null, ?ViewConfig $config = nu
$viewPath = $viewPath ?: (new Paths())->viewDirectory;
$config ??= config(ViewConfig::class);
- return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
+ return new Parser($config, $viewPath, AppServices::get('locator'), CI_DEBUG, AppServices::get('logger'));
}
/**
@@ -502,7 +504,7 @@ public static function renderer(?string $viewPath = null, ?ViewConfig $config =
$viewPath = $viewPath ?: (new Paths())->viewDirectory;
$config ??= config(ViewConfig::class);
- return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
+ return new View($config, $viewPath, AppServices::get('locator'), CI_DEBUG, AppServices::get('logger'));
}
/**
@@ -542,7 +544,7 @@ public static function createRequest(App $config, bool $isCli = false): void
$request->setProtocolVersion($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1');
}
- // Inject the request object into Services::request().
+ // Inject the request object into Services.
static::$instances['request'] = $request;
}
@@ -563,7 +565,7 @@ public static function incomingrequest(?App $config = null, bool $getShared = tr
return new IncomingRequest(
$config,
- AppServices::uri(),
+ AppServices::get('uri'),
'php://input',
new UserAgent()
);
@@ -598,7 +600,7 @@ public static function redirectresponse(?App $config = null, bool $getShared = t
$config ??= config(App::class);
$response = new RedirectResponse($config);
- $response->setProtocolVersion(AppServices::request()->getProtocolVersion());
+ $response->setProtocolVersion(AppServices::get('request')->getProtocolVersion());
return $response;
}
@@ -615,7 +617,7 @@ public static function routes(bool $getShared = true)
return static::getSharedInstance('routes');
}
- return new RouteCollection(AppServices::locator(), config(Modules::class), config(Routing::class));
+ return new RouteCollection(AppServices::get('locator'), config(Modules::class), config(Routing::class));
}
/**
@@ -630,8 +632,8 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request
return static::getSharedInstance('router', $routes, $request);
}
- $routes ??= AppServices::routes();
- $request ??= AppServices::request();
+ $routes ??= AppServices::get('routes');
+ $request ??= AppServices::get('request');
return new Router($routes, $request);
}
@@ -666,7 +668,7 @@ public static function session(?SessionConfig $config = null, bool $getShared =
$config ??= config(SessionConfig::class);
- $logger = AppServices::logger();
+ $logger = AppServices::get('logger');
$driverName = $config->driver;
@@ -683,7 +685,7 @@ public static function session(?SessionConfig $config = null, bool $getShared =
}
}
- $driver = new $driverName($config, AppServices::request()->getIPAddress());
+ $driver = new $driverName($config, AppServices::get('request')->getIPAddress());
$driver->setLogger($logger);
$session = new Session($driver, $config);
@@ -717,7 +719,7 @@ public static function siteurifactory(
}
$config ??= config('App');
- $superglobals ??= AppServices::superglobals();
+ $superglobals ??= AppServices::get('superglobals');
return new SiteURIFactory($config, $superglobals);
}
@@ -751,7 +753,7 @@ public static function throttler(bool $getShared = true)
return static::getSharedInstance('throttler');
}
- return new Throttler(AppServices::cache());
+ return new Throttler(AppServices::get('cache'));
}
/**
@@ -800,7 +802,7 @@ public static function uri(?string $uri = null, bool $getShared = true)
if ($uri === null) {
$appConfig = config(App::class);
- $factory = AppServices::siteurifactory($appConfig, AppServices::superglobals());
+ $factory = AppServices::siteurifactory($appConfig, AppServices::get('superglobals'));
return $factory->createFromGlobals();
}
@@ -821,7 +823,7 @@ public static function validation(?ValidationConfig $config = null, bool $getSha
$config ??= config(ValidationConfig::class);
- return new Validation($config, AppServices::renderer());
+ return new Validation($config, AppServices::get('renderer'));
}
/**
@@ -836,7 +838,7 @@ public static function viewcell(bool $getShared = true)
return static::getSharedInstance('viewcell');
}
- return new Cell(AppServices::cache());
+ return new Cell(AppServices::get('cache'));
}
/**
diff --git a/system/Config/View.php b/system/Config/View.php
index ea1442e5bc62..038c6fa774dc 100644
--- a/system/Config/View.php
+++ b/system/Config/View.php
@@ -1,5 +1,7 @@
setTtl($time);
- }
-
- /**
- * Handles "auto-loading" helper files.
- *
- * @deprecated Use `helper` function instead of using this method.
- *
- * @codeCoverageIgnore
- *
- * @return void
- */
- protected function loadHelpers()
- {
- if ($this->helpers === []) {
- return;
- }
-
- helper($this->helpers);
+ service('responsecache')->setTtl($time);
}
/**
@@ -174,7 +157,7 @@ protected function validateData(array $data, $rules, array $messages = [], ?stri
*/
private function setValidator($rules, array $messages): void
{
- $this->validator = Services::validation();
+ $this->validator = service('validation');
// If you replace the $rules array with the name of the group
if (is_string($rules)) {
diff --git a/system/Cookie/CloneableCookieInterface.php b/system/Cookie/CloneableCookieInterface.php
index 93f6031e2010..0b7d6fdf5015 100644
--- a/system/Cookie/CloneableCookieInterface.php
+++ b/system/Cookie/CloneableCookieInterface.php
@@ -1,5 +1,7 @@
$cookie) {
- $type = is_object($cookie) ? get_class($cookie) : gettype($cookie);
+ $type = get_debug_type($cookie);
if (! $cookie instanceof Cookie) {
throw CookieException::forInvalidCookieInstance([static::class, Cookie::class, $type, $index]);
diff --git a/system/Cookie/Exceptions/CookieException.php b/system/Cookie/Exceptions/CookieException.php
index af466061155b..8b6c7575983a 100644
--- a/system/Cookie/Exceptions/CookieException.php
+++ b/system/Cookie/Exceptions/CookieException.php
@@ -1,5 +1,7 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Class ArrayCast
+ *
+ * (PHP) [array --> string] --> (DB driver) --> (DB column) string
+ * [ <-- string] <-- (DB driver) <-- (DB column) string
+ */
+class ArrayCast extends BaseCast implements CastInterface
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): array {
+ if (! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ if ((str_starts_with($value, 'a:') || str_starts_with($value, 's:'))) {
+ $value = unserialize($value, ['allowed_classes' => false]);
+ }
+
+ return (array) $value;
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): string {
+ return serialize($value);
+ }
+}
diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php
new file mode 100644
index 000000000000..c3df0efee103
--- /dev/null
+++ b/system/DataCaster/Cast/BaseCast.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+use InvalidArgumentException;
+
+abstract class BaseCast implements CastInterface
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): mixed {
+ return $value;
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): mixed {
+ return $value;
+ }
+
+ protected static function invalidTypeValueError(mixed $value): never
+ {
+ $message = '[' . static::class . '] Invalid value type: ' . get_debug_type($value);
+ if (is_scalar($value)) {
+ $message .= ', and its value: ' . var_export($value, true);
+ }
+
+ throw new InvalidArgumentException($message);
+ }
+}
diff --git a/system/DataCaster/Cast/BooleanCast.php b/system/DataCaster/Cast/BooleanCast.php
new file mode 100644
index 000000000000..e4a3fde09345
--- /dev/null
+++ b/system/DataCaster/Cast/BooleanCast.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Class BooleanCast
+ *
+ * (PHP) [bool --> bool ] --> (DB driver) --> (DB column) bool|int(0/1)
+ * [ <-- string|int] <-- (DB driver) <-- (DB column) bool|int(0/1)
+ */
+class BooleanCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): bool {
+ // For PostgreSQL
+ if ($value === 't') {
+ return true;
+ }
+ if ($value === 'f') {
+ return false;
+ }
+
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+}
diff --git a/system/DataCaster/Cast/CSVCast.php b/system/DataCaster/Cast/CSVCast.php
new file mode 100644
index 000000000000..42dd3709a1db
--- /dev/null
+++ b/system/DataCaster/Cast/CSVCast.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Class CSVCast
+ *
+ * (PHP) [array --> string] --> (DB driver) --> (DB column) string
+ * [ <-- string] <-- (DB driver) <-- (DB column) string
+ */
+class CSVCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): array {
+ if (! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return explode(',', $value);
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): string {
+ if (! is_array($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return implode(',', $value);
+ }
+}
diff --git a/system/DataCaster/Cast/CastInterface.php b/system/DataCaster/Cast/CastInterface.php
new file mode 100644
index 000000000000..ff93dc2860bc
--- /dev/null
+++ b/system/DataCaster/Cast/CastInterface.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+interface CastInterface
+{
+ /**
+ * Takes a value from DataSource, returns its value for PHP.
+ *
+ * @param mixed $value Data from database driver
+ * @param list $params Additional param
+ * @param object|null $helper Helper object. E.g., database connection
+ *
+ * @return mixed PHP native value
+ */
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): mixed;
+
+ /**
+ * Takes a PHP value, returns its value for DataSource.
+ *
+ * @param mixed $value PHP native value
+ * @param list $params Additional param
+ * @param object|null $helper Helper object. E.g., database connection
+ *
+ * @return mixed Data to pass to database driver
+ */
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): mixed;
+}
diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php
new file mode 100644
index 000000000000..83e66d02217c
--- /dev/null
+++ b/system/DataCaster/Cast/DatetimeCast.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+use CodeIgniter\Database\BaseConnection;
+use CodeIgniter\I18n\Time;
+use InvalidArgumentException;
+
+/**
+ * Class DatetimeCast
+ *
+ * (PHP) [Time --> string] --> (DB driver) --> (DB column) datetime
+ * [ <-- string] <-- (DB driver) <-- (DB column) datetime
+ */
+class DatetimeCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): Time {
+ if (! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ if (! $helper instanceof BaseConnection) {
+ $message = 'The parameter $helper must be BaseConnection.';
+
+ throw new InvalidArgumentException($message);
+ }
+
+ /**
+ * @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters
+ */
+ $format = match ($params[0] ?? '') {
+ '' => $helper->dateFormat['datetime'],
+ 'ms' => $helper->dateFormat['datetime-ms'],
+ 'us' => $helper->dateFormat['datetime-us'],
+ default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]),
+ };
+
+ return Time::createFromFormat($format, $value);
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): string {
+ if (! $value instanceof Time) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (string) $value;
+ }
+}
diff --git a/system/DataCaster/Cast/FloatCast.php b/system/DataCaster/Cast/FloatCast.php
new file mode 100644
index 000000000000..7ced2e2653a7
--- /dev/null
+++ b/system/DataCaster/Cast/FloatCast.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Class FloatCast
+ *
+ * (PHP) [float --> float ] --> (DB driver) --> (DB column) float
+ * [ <-- float|string] <-- (DB driver) <-- (DB column) float
+ */
+class FloatCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): float {
+ if (! is_float($value) && ! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (float) $value;
+ }
+}
diff --git a/system/DataCaster/Cast/IntBoolCast.php b/system/DataCaster/Cast/IntBoolCast.php
new file mode 100644
index 000000000000..56977c842f0e
--- /dev/null
+++ b/system/DataCaster/Cast/IntBoolCast.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Int Bool Cast
+ *
+ * (PHP) [bool --> int ] --> (DB driver) --> (DB column) int(0/1)
+ * [ <-- int|string] <-- (DB driver) <-- (DB column) int(0/1)
+ */
+final class IntBoolCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): bool {
+ if (! is_int($value) && ! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (bool) $value;
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): int {
+ if (! is_bool($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (int) $value;
+ }
+}
diff --git a/system/DataCaster/Cast/IntegerCast.php b/system/DataCaster/Cast/IntegerCast.php
new file mode 100644
index 000000000000..e16683b1fb8f
--- /dev/null
+++ b/system/DataCaster/Cast/IntegerCast.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+/**
+ * Class IntegerCast
+ *
+ * (PHP) [int --> int ] --> (DB driver) --> (DB column) int
+ * [ <-- int|string] <-- (DB driver) <-- (DB column) int
+ */
+class IntegerCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): int {
+ if (! is_string($value) && ! is_int($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (int) $value;
+ }
+}
diff --git a/system/DataCaster/Cast/JsonCast.php b/system/DataCaster/Cast/JsonCast.php
new file mode 100644
index 000000000000..316070aaedee
--- /dev/null
+++ b/system/DataCaster/Cast/JsonCast.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+use CodeIgniter\DataCaster\Exceptions\CastException;
+use JsonException;
+use stdClass;
+
+/**
+ * Class JsonCast
+ *
+ * (PHP) [array|stdClass --> string] --> (DB driver) --> (DB column) string
+ * [ <-- string] <-- (DB driver) <-- (DB column) string
+ */
+class JsonCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): array|stdClass {
+ if (! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ $associative = in_array('array', $params, true);
+
+ $output = ($associative ? [] : new stdClass());
+
+ try {
+ $output = json_decode($value, $associative, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ throw CastException::forInvalidJsonFormat($e->getCode());
+ }
+
+ return $output;
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): string {
+ try {
+ $output = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ throw CastException::forInvalidJsonFormat($e->getCode());
+ }
+
+ return $output;
+ }
+}
diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php
new file mode 100644
index 000000000000..52a4d88f9e46
--- /dev/null
+++ b/system/DataCaster/Cast/TimestampCast.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+use CodeIgniter\I18n\Time;
+
+/**
+ * Class TimestampCast
+ *
+ * (PHP) [Time --> int ] --> (DB driver) --> (DB column) int
+ * [ <-- int|string] <-- (DB driver) <-- (DB column) int
+ */
+class TimestampCast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): Time {
+ if (! is_int($value) && ! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return Time::createFromTimestamp((int) $value);
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): int {
+ if (! $value instanceof Time) {
+ self::invalidTypeValueError($value);
+ }
+
+ return $value->getTimestamp();
+ }
+}
diff --git a/system/DataCaster/Cast/URICast.php b/system/DataCaster/Cast/URICast.php
new file mode 100644
index 000000000000..63f4d2271a78
--- /dev/null
+++ b/system/DataCaster/Cast/URICast.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Cast;
+
+use CodeIgniter\HTTP\URI;
+
+/**
+ * Class URICast
+ *
+ * (PHP) [URI --> string] --> (DB driver) --> (DB column) string
+ * [ <-- string] <-- (DB driver) <-- (DB column) string
+ */
+class URICast extends BaseCast
+{
+ public static function get(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): URI {
+ if (! is_string($value)) {
+ self::invalidTypeValueError($value);
+ }
+
+ return new URI($value);
+ }
+
+ public static function set(
+ mixed $value,
+ array $params = [],
+ ?object $helper = null
+ ): string {
+ if (! $value instanceof URI) {
+ self::invalidTypeValueError($value);
+ }
+
+ return (string) $value;
+ }
+}
diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php
new file mode 100644
index 000000000000..854a0b50b32c
--- /dev/null
+++ b/system/DataCaster/DataCaster.php
@@ -0,0 +1,188 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster;
+
+use CodeIgniter\DataCaster\Cast\ArrayCast;
+use CodeIgniter\DataCaster\Cast\BooleanCast;
+use CodeIgniter\DataCaster\Cast\CastInterface;
+use CodeIgniter\DataCaster\Cast\CSVCast;
+use CodeIgniter\DataCaster\Cast\DatetimeCast;
+use CodeIgniter\DataCaster\Cast\FloatCast;
+use CodeIgniter\DataCaster\Cast\IntBoolCast;
+use CodeIgniter\DataCaster\Cast\IntegerCast;
+use CodeIgniter\DataCaster\Cast\JsonCast;
+use CodeIgniter\DataCaster\Cast\TimestampCast;
+use CodeIgniter\DataCaster\Cast\URICast;
+use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface;
+use CodeIgniter\Entity\Exceptions\CastException;
+use InvalidArgumentException;
+
+final class DataCaster
+{
+ /**
+ * Array of field names and the type of value to cast.
+ *
+ * @var array [field => type]
+ */
+ private array $types = [];
+
+ /**
+ * Convert handlers
+ *
+ * @var array [type => classname]
+ */
+ private array $castHandlers = [
+ 'array' => ArrayCast::class,
+ 'bool' => BooleanCast::class,
+ 'boolean' => BooleanCast::class,
+ 'csv' => CSVCast::class,
+ 'datetime' => DatetimeCast::class,
+ 'double' => FloatCast::class,
+ 'float' => FloatCast::class,
+ 'int' => IntegerCast::class,
+ 'integer' => IntegerCast::class,
+ 'int-bool' => IntBoolCast::class,
+ 'json' => JsonCast::class,
+ 'timestamp' => TimestampCast::class,
+ 'uri' => URICast::class,
+ ];
+
+ /**
+ * @param array|null $castHandlers Custom convert handlers
+ * @param array|null $types [field => type]
+ * @param object|null $helper Helper object.
+ * @param bool $strict Strict mode? Set to false for casts for Entity.
+ */
+ public function __construct(
+ ?array $castHandlers = null,
+ ?array $types = null,
+ private readonly ?object $helper = null,
+ private readonly bool $strict = true
+ ) {
+ $this->castHandlers = array_merge($this->castHandlers, $castHandlers);
+
+ if ($types !== null) {
+ $this->setTypes($types);
+ }
+
+ if ($this->strict) {
+ foreach ($this->castHandlers as $handler) {
+ if (
+ ! is_subclass_of($handler, CastInterface::class)
+ && ! is_subclass_of($handler, EntityCastInterface::class)
+ ) {
+ throw new InvalidArgumentException(
+ 'Invalid class type. It must implement CastInterface. class: ' . $handler
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is only for Entity.
+ *
+ * @TODO if Entity::$casts is readonly, we don't need this method.
+ *
+ * @param array $types [field => type]
+ *
+ * @return $this
+ *
+ * @internal
+ */
+ public function setTypes(array $types): static
+ {
+ $this->types = $types;
+
+ return $this;
+ }
+
+ /**
+ * Provides the ability to cast an item as a specific data type.
+ * Add ? at the beginning of the type (i.e. ?string) to get `null`
+ * instead of casting $value when $value is null.
+ *
+ * @param mixed $value The value to convert
+ * @param string $field The field name
+ * @param string $method Allowed to "get" and "set"
+ * @phpstan-param 'get'|'set' $method
+ */
+ public function castAs(mixed $value, string $field, string $method = 'get'): mixed
+ {
+ // If the type is not defined, return as it is.
+ if (! isset($this->types[$field])) {
+ return $value;
+ }
+
+ $type = $this->types[$field];
+
+ $isNullable = false;
+
+ // Is nullable?
+ if (str_starts_with($type, '?')) {
+ $isNullable = true;
+
+ if ($value === null) {
+ return null;
+ }
+
+ $type = substr($type, 1);
+ } elseif ($value === null) {
+ if ($this->strict) {
+ $message = 'Field "' . $field . '" is not nullable, but null was passed.';
+
+ throw new InvalidArgumentException($message);
+ }
+ }
+
+ // In order not to create a separate handler for the
+ // json-array type, we transform the required one.
+ $type = ($type === 'json-array') ? 'json[array]' : $type;
+
+ $params = [];
+
+ // Attempt to retrieve additional parameters if specified
+ // type[param, param2,param3]
+ if (preg_match('/\A(.+)\[(.+)\]\z/', $type, $matches)) {
+ $type = $matches[1];
+ $params = array_map('trim', explode(',', $matches[2]));
+ }
+
+ if ($isNullable) {
+ $params[] = 'nullable';
+ }
+
+ $type = trim($type, '[]');
+
+ $handlers = $this->castHandlers;
+
+ if (! isset($handlers[$type])) {
+ throw new InvalidArgumentException(
+ 'No such handler for "' . $field . '". Invalid type: ' . $type
+ );
+ }
+
+ $handler = $handlers[$type];
+
+ if (
+ ! $this->strict
+ && ! is_subclass_of($handler, CastInterface::class)
+ && ! is_subclass_of($handler, EntityCastInterface::class)
+ ) {
+ throw CastException::forInvalidInterface($handler);
+ }
+
+ return $handler::$method($value, $params, $this->helper);
+ }
+}
diff --git a/system/DataCaster/Exceptions/CastException.php b/system/DataCaster/Exceptions/CastException.php
new file mode 100644
index 000000000000..ff328b3deda1
--- /dev/null
+++ b/system/DataCaster/Exceptions/CastException.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataCaster\Exceptions;
+
+use CodeIgniter\Entity\Exceptions\CastException as EntityCastException;
+
+/**
+ * CastException is thrown for invalid cast initialization and management.
+ */
+class CastException extends EntityCastException
+{
+}
diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php
new file mode 100644
index 000000000000..b79ede5b08ff
--- /dev/null
+++ b/system/DataConverter/DataConverter.php
@@ -0,0 +1,202 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\DataConverter;
+
+use Closure;
+use CodeIgniter\DataCaster\DataCaster;
+use CodeIgniter\Entity\Entity;
+
+/**
+ * PHP data <==> DataSource data converter
+ *
+ * @see \CodeIgniter\DataConverter\DataConverterTest
+ *
+ * @template TEntity of object
+ */
+final class DataConverter
+{
+ /**
+ * The data caster.
+ */
+ private readonly DataCaster $dataCaster;
+
+ /**
+ * @param array $castHandlers Custom convert handlers
+ *
+ * @internal
+ */
+ public function __construct(
+ /**
+ * Type definitions.
+ *
+ * @var array [column => type]
+ */
+ private readonly array $types,
+ array $castHandlers = [],
+ /**
+ * Helper object.
+ */
+ private readonly ?object $helper = null,
+ /**
+ * Static reconstruct method name or closure to reconstruct an object.
+ * Used by reconstruct().
+ *
+ * @phpstan-var (Closure(array): TEntity)|string|null
+ */
+ private readonly Closure|string|null $reconstructor = 'reconstruct',
+ /**
+ * Extract method name or closure to extract data from an object.
+ * Used by extract().
+ *
+ * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null
+ */
+ private readonly Closure|string|null $extractor = null,
+ ) {
+ $this->dataCaster = new DataCaster($castHandlers, $types, $this->helper);
+ }
+
+ /**
+ * Converts data from DataSource to PHP array with specified type values.
+ *
+ * @param array $data DataSource data
+ *
+ * @internal
+ */
+ public function fromDataSource(array $data): array
+ {
+ foreach (array_keys($this->types) as $field) {
+ if (array_key_exists($field, $data)) {
+ $data[$field] = $this->dataCaster->castAs($data[$field], $field, 'get');
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Converts PHP array to data for DataSource field types.
+ *
+ * @param array $phpData PHP data
+ *
+ * @internal
+ */
+ public function toDataSource(array $phpData): array
+ {
+ foreach (array_keys($this->types) as $field) {
+ if (array_key_exists($field, $phpData)) {
+ $phpData[$field] = $this->dataCaster->castAs($phpData[$field], $field, 'set');
+ }
+ }
+
+ return $phpData;
+ }
+
+ /**
+ * Takes database data array and creates a specified type object.
+ *
+ * @param class-string $classname
+ * @phpstan-param class-string $classname
+ * @param array $row Raw data from database
+ *
+ * @phpstan-return TEntity
+ *
+ * @internal
+ */
+ public function reconstruct(string $classname, array $row): object
+ {
+ $phpData = $this->fromDataSource($row);
+
+ // Use static reconstruct method.
+ if (is_string($this->reconstructor) && method_exists($classname, $this->reconstructor)) {
+ $method = $this->reconstructor;
+
+ return $classname::$method($phpData);
+ }
+
+ // Use closure to reconstruct.
+ if ($this->reconstructor instanceof Closure) {
+ $closure = $this->reconstructor;
+
+ return $closure($phpData);
+ }
+
+ $classObj = new $classname();
+
+ if ($classObj instanceof Entity) {
+ $classObj->injectRawData($phpData);
+ $classObj->syncOriginal();
+
+ return $classObj;
+ }
+
+ $classSet = Closure::bind(function ($key, $value) {
+ $this->{$key} = $value;
+ }, $classObj, $classname);
+
+ foreach ($phpData as $key => $value) {
+ $classSet($key, $value);
+ }
+
+ return $classObj;
+ }
+
+ /**
+ * Takes an object and extract properties as an array.
+ *
+ * @param bool $onlyChanged Only for CodeIgniter's Entity. If true, only returns
+ * values that have changed since object creation.
+ * @param bool $recursive Only for CodeIgniter's Entity. If true, inner
+ * entities will be cast as array as well.
+ *
+ * @return array
+ *
+ * @internal
+ */
+ public function extract(object $object, bool $onlyChanged = false, bool $recursive = false): array
+ {
+ // Use extractor method.
+ if (is_string($this->extractor) && method_exists($object, $this->extractor)) {
+ $method = $this->extractor;
+ $row = $object->{$method}($onlyChanged, $recursive);
+
+ return $this->toDataSource($row);
+ }
+
+ // Use closure to extract.
+ if ($this->extractor instanceof Closure) {
+ $closure = $this->extractor;
+ $row = $closure($object, $onlyChanged, $recursive);
+
+ return $this->toDataSource($row);
+ }
+
+ if ($object instanceof Entity) {
+ $row = $object->toRawArray($onlyChanged, $recursive);
+
+ return $this->toDataSource($row);
+ }
+
+ $array = (array) $object;
+
+ $row = [];
+
+ foreach ($array as $key => $value) {
+ $key = preg_replace('/\000.*\000/', '', $key);
+
+ $row[$key] = $value;
+ }
+
+ return $this->toDataSource($row);
+ }
+}
diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
index 65dea86da2b3..1f0807cdbbde 100644
--- a/system/Database/BaseBuilder.php
+++ b/system/Database/BaseBuilder.php
@@ -1,5 +1,7 @@
db = $db;
// If it contains `,`, it has multiple tables
- if (is_string($tableName) && strpos($tableName, ',') === false) {
+ if (is_string($tableName) && ! str_contains($tableName, ',')) {
$this->tableName = $tableName; // @TODO remove alias if exists
} else {
$this->tableName = '';
@@ -511,7 +514,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string
throw DataException::forEmptyInputGiven('Select');
}
- if (strpos($select, ',') !== false) {
+ if (str_contains($select, ',')) {
throw DataException::forInvalidArgument('column name not separated by comma');
}
@@ -538,7 +541,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string
*/
protected function createAliasFromTable(string $item): string
{
- if (strpos($item, '.') !== false) {
+ if (str_contains($item, '.')) {
$item = explode('.', $item);
return end($item);
@@ -574,7 +577,7 @@ public function from($from, bool $overwrite = false): self
}
foreach ((array) $from as $table) {
- if (strpos($table, ',') !== false) {
+ if (str_contains($table, ',')) {
$this->from(explode(',', $table));
} else {
$table = trim($table);
@@ -763,7 +766,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type
$op = trim(current($op));
// Does the key end with operator?
- if (substr($k, -strlen($op)) === $op) {
+ if (str_ends_with($k, $op)) {
$k = rtrim(substr($k, 0, -strlen($op)));
$op = " {$op}";
} else {
@@ -1490,6 +1493,11 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape =
*/
public function limit(?int $value = null, ?int $offset = 0)
{
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $value === 0) {
+ $value = null;
+ }
+
if ($value !== null) {
$this->QBLimit = $value;
}
@@ -1607,6 +1615,11 @@ protected function compileFinalQuery(string $sql): string
*/
public function get(?int $limit = null, int $offset = 0, bool $reset = true)
{
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $limit === 0) {
+ $limit = null;
+ }
+
if ($limit !== null) {
$this->limit($limit, $offset);
}
@@ -1740,7 +1753,12 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo
$this->where($where);
}
- if ($limit !== null && $limit !== 0) {
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $limit === 0) {
+ $limit = null;
+ }
+
+ if ($limit !== null) {
$this->limit($limit, $offset);
}
@@ -2324,7 +2342,7 @@ public function insert($set = null, ?bool $escape = null)
*/
protected function removeAlias(string $from): string
{
- if (strpos($from, ' ') !== false) {
+ if (str_contains($from, ' ')) {
// if the alias is written with the AS keyword, remove it
$from = preg_replace('/\s+AS\s+/i', ' ', $from);
@@ -2458,7 +2476,12 @@ public function update($set = null, $where = null, ?int $limit = null): bool
$this->where($where);
}
- if ($limit !== null && $limit !== 0) {
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $limit === 0) {
+ $limit = null;
+ }
+
+ if ($limit !== null) {
if (! $this->canLimitWhereUpdates) {
throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.');
}
@@ -2499,10 +2522,18 @@ protected function _update(string $table, array $values): string
$valStr[] = $key . ' = ' . $val;
}
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll) {
+ return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
+ . $this->compileWhereHaving('QBWhere')
+ . $this->compileOrderBy()
+ . ($this->QBLimit ? $this->_limit(' ', true) : '');
+ }
+
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
. $this->compileWhereHaving('QBWhere')
. $this->compileOrderBy()
- . ($this->QBLimit ? $this->_limit(' ', true) : '');
+ . ($this->QBLimit !== false ? $this->_limit(' ', true) : '');
}
/**
@@ -2768,7 +2799,12 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true)
$sql = $this->_delete($this->removeAlias($table));
- if ($limit !== null && $limit !== 0) {
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $limit === 0) {
+ $limit = null;
+ }
+
+ if ($limit !== null) {
$this->QBLimit = $limit;
}
@@ -2972,12 +3008,12 @@ protected function trackAliases($table)
// Does the string contain a comma? If so, we need to separate
// the string into discreet statements
- if (strpos($table, ',') !== false) {
+ if (str_contains($table, ',')) {
return $this->trackAliases(explode(',', $table));
}
// if a table alias is used we can recognize it by a space
- if (strpos($table, ' ') !== false) {
+ if (str_contains($table, ' ')) {
// if the alias is written with the AS keyword, remove it
$table = preg_replace('/\s+AS\s+/i', ' ', $table);
@@ -3034,7 +3070,12 @@ protected function compileSelect($selectOverride = false): string
. $this->compileWhereHaving('QBHaving')
. $this->compileOrderBy();
- if ($this->QBLimit) {
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll) {
+ if ($this->QBLimit) {
+ $sql = $this->_limit($sql . "\n");
+ }
+ } elseif ($this->QBLimit !== false || $this->QBOffset) {
$sql = $this->_limit($sql . "\n");
}
@@ -3117,11 +3158,11 @@ protected function compileWhereHaving(string $qbKey): string
if (! empty($matches[4])) {
$protectIdentifiers = false;
- if (strpos($matches[4], '.') !== false) {
+ if (str_contains($matches[4], '.')) {
$protectIdentifiers = true;
}
- if (strpos($matches[4], ':') === false) {
+ if (! str_contains($matches[4], ':')) {
$matches[4] = $this->db->protectIdentifiers(trim($matches[4]), false, $protectIdentifiers);
}
diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
index 93bde5ddca6d..e0bd7bb51217 100644
--- a/system/Database/BaseConnection.php
+++ b/system/Database/BaseConnection.php
@@ -1,5 +1,7 @@
+ */
+ protected array $dateFormat = [
+ 'date' => 'Y-m-d',
+ 'datetime' => 'Y-m-d H:i:s',
+ 'datetime-ms' => 'Y-m-d H:i:s.v',
+ 'datetime-us' => 'Y-m-d H:i:s.u',
+ 'time' => 'H:i:s',
+ ];
+
/**
* Saves our connection settings.
*/
public function __construct(array $params)
{
+ if (isset($params['dateFormat'])) {
+ $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
+ unset($params['dateFormat']);
+ }
+
foreach ($params as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
@@ -1001,10 +1029,10 @@ public function getConnectDuration(int $decimals = 6): string
* insert the table prefix (if it exists) in the proper position, and escape only
* the correct identifiers.
*
- * @param array|string $item
- * @param bool $prefixSingle Prefix a table name with no segments?
- * @param bool $protectIdentifiers Protect table or column names?
- * @param bool $fieldExists Supplied $item contains a column name?
+ * @param array|int|string $item
+ * @param bool $prefixSingle Prefix a table name with no segments?
+ * @param bool $protectIdentifiers Protect table or column names?
+ * @param bool $fieldExists Supplied $item contains a column name?
*
* @return array|string
* @phpstan-return ($item is array ? array : string)
@@ -1025,6 +1053,9 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro
return $escapedArray;
}
+ // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
+ $item = (string) $item;
+
// This is basically a bug fix for queries that use MAX, MIN, etc.
// If a parenthesis is found we know that we do not need to
// escape the data or add a prefix. There's probably a more graceful
@@ -1062,7 +1093,7 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro
// Break the string apart if it contains periods, then insert the table prefix
// in the correct location, assuming the period doesn't indicate that we're dealing
// with an alias. While we're at it, we will escape the components
- if (strpos($item, '.') !== false) {
+ if (str_contains($item, '.')) {
return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
}
@@ -1074,11 +1105,11 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro
// Is there a table prefix? If not, no need to insert it
if ($this->DBPrefix !== '') {
// Verify table prefix and replace if necessary
- if ($this->swapPre !== '' && strpos($item, $this->swapPre) === 0) {
+ if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
$item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
}
// Do we prefix an item with no segments?
- elseif ($prefixSingle === true && strpos($item, $this->DBPrefix) !== 0) {
+ elseif ($prefixSingle === true && ! str_starts_with($item, $this->DBPrefix)) {
$item = $this->DBPrefix . $item;
}
}
@@ -1140,11 +1171,11 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti
}
// Verify table prefix and replace if necessary
- if ($this->swapPre !== '' && strpos($parts[$i], $this->swapPre) === 0) {
+ if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
$parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
}
// We only add the table prefix if it does not already exist
- elseif (strpos($parts[$i], $this->DBPrefix) !== 0) {
+ elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
$parts[$i] = $this->DBPrefix . $parts[$i];
}
@@ -1159,6 +1190,24 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti
return $item . $alias;
}
+ /**
+ * Escape the SQL Identifier
+ *
+ * This function escapes single identifier.
+ *
+ * @param non-empty-string $item
+ */
+ public function escapeIdentifier(string $item): string
+ {
+ return $this->escapeChar
+ . str_replace(
+ $this->escapeChar,
+ $this->escapeChar . $this->escapeChar,
+ $item
+ )
+ . $this->escapeChar;
+ }
+
/**
* Escape the SQL Identifiers
*
@@ -1187,7 +1236,7 @@ public function escapeIdentifiers($item)
if (ctype_digit($item)
|| $item[0] === "'"
|| ($this->escapeChar !== '"' && $item[0] === '"')
- || strpos($item, '(') !== false) {
+ || str_contains($item, '(')) {
return $item;
}
@@ -1207,7 +1256,7 @@ public function escapeIdentifiers($item)
foreach ($this->reservedIdentifiers as $id) {
/** @psalm-suppress NoValue I don't know why ERROR. */
- if (strpos($item, '.' . $id) !== false) {
+ if (str_contains($item, '.' . $id)) {
return preg_replace(
'/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
$this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
@@ -1257,7 +1306,7 @@ abstract public function affectedRows(): int;
public function escape($str)
{
if (is_array($str)) {
- return array_map([&$this, 'escape'], $str);
+ return array_map($this->escape(...), $str);
}
/** @psalm-suppress NoValue I don't know why ERROR. */
@@ -1353,7 +1402,7 @@ public function callFunction(string $functionName, ...$params): bool
{
$driver = $this->getDriverFunctionPrefix();
- if (strpos($driver, $functionName) === false) {
+ if (! str_contains($driver, $functionName)) {
$functionName = $driver . $functionName;
}
diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php
index ea16de594c2b..8c4f252cfb7e 100644
--- a/system/Database/BasePreparedQuery.php
+++ b/system/Database/BasePreparedQuery.php
@@ -1,5 +1,7 @@
db->isWriteType($query)) {
+ if ($this->db->isWriteType((string) $query)) {
return true;
}
diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php
index 8720b5fe111c..efbc16722148 100644
--- a/system/Database/BaseResult.php
+++ b/system/Database/BaseResult.php
@@ -1,5 +1,7 @@
' . $newline;
foreach ($row as $key => $val) {
- $val = (! empty($val)) ? xml_convert($val) : '';
+ $val = (! empty($val)) ? xml_convert((string) $val) : '';
$xml .= $tab . $tab . '<' . $key . '>' . $val . '' . $key . '>' . $newline;
}
diff --git a/system/Database/Config.php b/system/Database/Config.php
index 92556c6d8fef..03a0dd15743b 100644
--- a/system/Database/Config.php
+++ b/system/Database/Config.php
@@ -1,5 +1,7 @@
{$group})) {
- throw new InvalidArgumentException($group . ' is not a valid database connection group.');
+ throw new InvalidArgumentException('"' . $group . '" is not a valid database connection group.');
}
$config = $dbConfig->{$group};
diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php
index ed45933734a2..2f1c932c461d 100644
--- a/system/Database/ConnectionInterface.php
+++ b/system/Database/ConnectionInterface.php
@@ -1,5 +1,7 @@
parseDSN($params);
}
@@ -130,7 +132,7 @@ protected function parseDSN(array $params): array
*/
protected function initDriver(string $driver, string $class, $argument): object
{
- $classname = (strpos($driver, '\\') === false)
+ $classname = (! str_contains($driver, '\\'))
? "CodeIgniter\\Database\\{$driver}\\{$class}"
: $driver . '\\' . $class;
diff --git a/system/Database/Exceptions/DataException.php b/system/Database/Exceptions/DataException.php
index 09b9ff329708..18a54171cc29 100644
--- a/system/Database/Exceptions/DataException.php
+++ b/system/Database/Exceptions/DataException.php
@@ -1,5 +1,7 @@
db->query(sprintf($ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr, $dbName, $this->db->charset, $this->db->DBCollat))) {
+ if (! $this->db->query(
+ sprintf(
+ $ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr,
+ $this->db->escapeIdentifier($dbName),
+ $this->db->charset,
+ $this->db->DBCollat
+ )
+ )) {
// @codeCoverageIgnoreStart
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to create the specified database.');
@@ -284,7 +293,9 @@ public function dropDatabase(string $dbName): bool
return false;
}
- if (! $this->db->query(sprintf($this->dropDatabaseStr, $dbName))) {
+ if (! $this->db->query(
+ sprintf($this->dropDatabaseStr, $this->db->escapeIdentifier($dbName))
+ )) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to drop the specified database.');
}
@@ -293,7 +304,11 @@ public function dropDatabase(string $dbName): bool
}
if (! empty($this->db->dataCache['db_names'])) {
- $key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
+ $key = array_search(
+ strtolower($dbName),
+ array_map('strtolower', $this->db->dataCache['db_names']),
+ true
+ );
if ($key !== false) {
unset($this->db->dataCache['db_names'][$key]);
}
@@ -368,7 +383,7 @@ public function addField($fields)
]);
$this->addKey('id', true);
} else {
- if (strpos($fields, ' ') === false) {
+ if (! str_contains($fields, ' ')) {
throw new InvalidArgumentException('Field information is required for that operation.');
}
@@ -635,7 +650,7 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca
return false;
}
- if ($this->db->DBPrefix && strpos($tableName, $this->db->DBPrefix) === 0) {
+ if ($this->db->DBPrefix && str_starts_with($tableName, $this->db->DBPrefix)) {
$tableName = substr($tableName, strlen($this->db->DBPrefix));
}
diff --git a/system/Database/Migration.php b/system/Database/Migration.php
index f12509d6264a..4386b7448e80 100644
--- a/system/Database/Migration.php
+++ b/system/Database/Migration.php
@@ -1,5 +1,7 @@
get()
->getResultObject();
- return count($migration) ? $migration[0]->version : 0;
+ return $migration === [] ? '0' : $migration[0]->version;
}
/**
diff --git a/system/Database/ModelFactory.php b/system/Database/ModelFactory.php
deleted file mode 100644
index 76a30e6c1d30..000000000000
--- a/system/Database/ModelFactory.php
+++ /dev/null
@@ -1,53 +0,0 @@
-
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Database;
-
-use CodeIgniter\Config\Factories;
-
-/**
- * Returns new or shared Model instances
- *
- * @deprecated Use CodeIgniter\Config\Factories::models()
- *
- * @codeCoverageIgnore
- * @see \CodeIgniter\Database\ModelFactoryTest
- */
-class ModelFactory
-{
- /**
- * Creates new Model instances or returns a shared instance
- *
- * @return mixed
- */
- public static function get(string $name, bool $getShared = true, ?ConnectionInterface $connection = null)
- {
- return Factories::models($name, ['getShared' => $getShared], $connection);
- }
-
- /**
- * Helper method for injecting mock instances while testing.
- *
- * @param object $instance
- */
- public static function injectMock(string $name, $instance)
- {
- Factories::injectMock('models', $name, $instance);
- }
-
- /**
- * Resets the static arrays
- */
- public static function reset()
- {
- Factories::reset('models');
- }
-}
diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php
index 096bbedd888c..81434bc3645d 100644
--- a/system/Database/MySQLi/Builder.php
+++ b/system/Database/MySQLi/Builder.php
@@ -1,5 +1,7 @@
escapeIdentifiers($this->database);
+ $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
if ($tableName !== null) {
return $sql . ' LIKE ' . $this->escape($tableName);
diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php
index adc6c6cc7267..132bde1258cc 100644
--- a/system/Database/MySQLi/Forge.php
+++ b/system/Database/MySQLi/Forge.php
@@ -1,5 +1,7 @@
db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) {
+ if ($this->db->charset !== '' && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) {
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
}
- if (! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE')) {
+ if ($this->db->DBCollat !== '' && ! strpos($sql, 'COLLATE')) {
$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
}
diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php
index 594fcba24901..e9a6c6d10e30 100644
--- a/system/Database/MySQLi/PreparedQuery.php
+++ b/system/Database/MySQLi/PreparedQuery.php
@@ -1,5 +1,7 @@
username;
@@ -287,7 +289,7 @@ protected function _listColumns(string $table = ''): string
*/
protected function _fieldData(string $table): array
{
- if (strpos($table, '.') !== false) {
+ if (str_contains($table, '.')) {
sscanf($table, '%[^.].%s', $owner, $table);
} else {
$owner = $this->username;
@@ -331,7 +333,7 @@ protected function _fieldData(string $table): array
*/
protected function _indexData(string $table): array
{
- if (strpos($table, '.') !== false) {
+ if (str_contains($table, '.')) {
sscanf($table, '%[^.].%s', $owner, $table);
} else {
$owner = $this->username;
@@ -629,7 +631,7 @@ protected function buildDSN()
return;
}
- $isEasyConnectableHostName = $this->hostname !== '' && strpos($this->hostname, '/') === false && strpos($this->hostname, ':') === false;
+ $isEasyConnectableHostName = $this->hostname !== '' && ! str_contains($this->hostname, '/') && ! str_contains($this->hostname, ':');
$easyConnectablePort = ! empty($this->port) && ctype_digit($this->port) ? ':' . $this->port : '';
$easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : '';
diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php
index 4de36ac91e6d..69a42ed1dbeb 100644
--- a/system/Database/OCI8/Forge.php
+++ b/system/Database/OCI8/Forge.php
@@ -1,5 +1,7 @@
db->escapeIdentifiers($processedField['name'])
. ' IN ' . $processedField['length'] . ')';
diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php
index 93d260707fb0..c267f8061377 100644
--- a/system/Database/OCI8/PreparedQuery.php
+++ b/system/Database/OCI8/PreparedQuery.php
@@ -1,5 +1,7 @@
$value) {
- if (strtoupper($value[$key]) === 'NULL') {
+ if (strtoupper((string) $value[$key]) === 'NULL') {
$values[$arrayKey][$key] = 'DEFAULT';
}
}
diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php
index 7da3d26c9dd8..45e30eb65365 100644
--- a/system/Database/Postgre/Connection.php
+++ b/system/Database/Postgre/Connection.php
@@ -1,5 +1,7 @@
indexdef)));
$obj->fields = array_map(static fn ($v) => trim($v), $_fields);
- if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) {
+ if (str_starts_with($row->indexdef, 'CREATE UNIQUE INDEX pk')) {
$obj->type = 'PRIMARY';
} else {
- $obj->type = (strpos($row->indexdef, 'CREATE UNIQUE') === 0) ? 'UNIQUE' : 'INDEX';
+ $obj->type = (str_starts_with($row->indexdef, 'CREATE UNIQUE')) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
@@ -496,7 +498,7 @@ protected function buildDSN()
}
// If UNIX sockets are used, we shouldn't set a port
- if (strpos($this->hostname, '/') !== false) {
+ if (str_contains($this->hostname, '/')) {
$this->port = '';
}
diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php
index 8af1507c6b2a..7a61bed5a57c 100644
--- a/system/Database/Postgre/Forge.php
+++ b/system/Database/Postgre/Forge.php
@@ -1,5 +1,7 @@
QBFrom as $value) {
- $from[] = strpos($value, '(SELECT') === 0 ? $value : $this->getFullName($value);
+ $from[] = str_starts_with($value, '(SELECT') ? $value : $this->getFullName($value);
}
return implode(', ', $from);
@@ -280,7 +283,7 @@ private function getFullName(string $table): string
{
$alias = '';
- if (strpos($table, ' ') !== false) {
+ if (str_contains($table, ' ')) {
$alias = explode(' ', $table);
$table = array_shift($alias);
$alias = ' ' . implode(' ', $alias);
@@ -306,6 +309,15 @@ private function addIdentity(string $fullTable, string $insert): string
*/
protected function _limit(string $sql, bool $offsetIgnore = false): string
{
+ // SQL Server cannot handle `LIMIT 0`.
+ // DatabaseException:
+ // [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The number of
+ // rows provided for a FETCH clause must be greater then zero.
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if (! $limitZeroAsAll && $this->QBLimit === 0) {
+ return "SELECT * \nFROM " . $this->_fromTables() . ' WHERE 1=0 ';
+ }
+
if (empty($this->QBOrderBy)) {
$sql .= ' ORDER BY (SELECT NULL) ';
}
@@ -439,7 +451,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string
throw DataException::forEmptyInputGiven('Select');
}
- if (strpos($select, ',') !== false) {
+ if (str_contains($select, ',')) {
throw DataException::forInvalidArgument('Column name not separated by comma');
}
@@ -588,7 +600,12 @@ protected function compileSelect($selectOverride = false): string
. $this->compileOrderBy(); // ORDER BY
// LIMIT
- if ($this->QBLimit) {
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll) {
+ if ($this->QBLimit) {
+ $sql = $this->_limit($sql . "\n");
+ }
+ } elseif ($this->QBLimit !== false || $this->QBOffset) {
$sql = $this->_limit($sql . "\n");
}
@@ -603,6 +620,11 @@ protected function compileSelect($selectOverride = false): string
*/
public function get(?int $limit = null, int $offset = 0, bool $reset = true)
{
+ $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
+ if ($limitZeroAsAll && $limit === 0) {
+ $limit = null;
+ }
+
if ($limit !== null) {
$this->limit($limit, $offset);
}
diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php
index 77a1ab34bcf3..411470d8e34c 100755
--- a/system/Database/SQLSRV/Connection.php
+++ b/system/Database/SQLSRV/Connection.php
@@ -1,5 +1,7 @@
hostname, ',') === false && $this->port !== '') {
+ if (! str_contains($this->hostname, ',') && $this->port !== '') {
$this->hostname .= ', ' . $this->port;
}
@@ -190,7 +192,7 @@ protected function _escapeString(string $str): string
*/
public function insertID(): int
{
- return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0;
+ return (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0);
}
/**
@@ -253,10 +255,10 @@ protected function _indexData(string $table): array
$_fields = explode(',', trim($row->index_keys));
$obj->fields = array_map(static fn ($v) => trim($v), $_fields);
- if (strpos($row->index_description, 'primary key located on') !== false) {
+ if (str_contains($row->index_description, 'primary key located on')) {
$obj->type = 'PRIMARY';
} else {
- $obj->type = (strpos($row->index_description, 'nonclustered, unique') !== false) ? 'UNIQUE' : 'INDEX';
+ $obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php
index 5dead5b3a308..cae4eda2a4f5 100755
--- a/system/Database/SQLSRV/Forge.php
+++ b/system/Database/SQLSRV/Forge.php
@@ -1,5 +1,7 @@
dropIndexStr = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s';
}
+ /**
+ * Create database
+ *
+ * @param bool $ifNotExists Whether to add IF NOT EXISTS condition
+ *
+ * @throws DatabaseException
+ */
+ public function createDatabase(string $dbName, bool $ifNotExists = false): bool
+ {
+ if ($ifNotExists) {
+ $sql = sprintf(
+ $this->createDatabaseIfStr,
+ $dbName,
+ $this->db->escapeIdentifier($dbName)
+ );
+ } else {
+ $sql = sprintf(
+ $this->createDatabaseStr,
+ $this->db->escapeIdentifier($dbName)
+ );
+ }
+
+ try {
+ if (! $this->db->query($sql)) {
+ // @codeCoverageIgnoreStart
+ if ($this->db->DBDebug) {
+ throw new DatabaseException('Unable to create the specified database.');
+ }
+
+ return false;
+ // @codeCoverageIgnoreEnd
+ }
+
+ if (isset($this->db->dataCache['db_names'])) {
+ $this->db->dataCache['db_names'][] = $dbName;
+ }
+
+ return true;
+ } catch (Throwable $e) {
+ if ($this->db->DBDebug) {
+ throw new DatabaseException('Unable to create the specified database.', 0, $e);
+ }
+
+ return false; // @codeCoverageIgnore
+ }
+ }
+
/**
* CREATE TABLE attributes
*/
@@ -324,8 +375,18 @@ protected function _attributeType(array &$attributes)
break;
case 'ENUM':
- $attributes['TYPE'] = 'TEXT';
- $attributes['CONSTRAINT'] = null;
+ // in char(n) and varchar(n), the n defines the string length in
+ // bytes (0 to 8,000).
+ // https://learn.microsoft.com/en-us/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver16#remarks
+ $maxLength = max(
+ array_map(
+ static fn ($value) => strlen($value),
+ $attributes['CONSTRAINT']
+ )
+ );
+
+ $attributes['TYPE'] = 'VARCHAR';
+ $attributes['CONSTRAINT'] = $maxLength;
break;
case 'TIMESTAMP':
diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php
index f752e769349b..f3a4a14c2bc6 100755
--- a/system/Database/SQLSRV/PreparedQuery.php
+++ b/system/Database/SQLSRV/PreparedQuery.php
@@ -1,5 +1,7 @@
database !== ':memory:' && strpos($this->database, DIRECTORY_SEPARATOR) === false) {
+ if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
$this->database = WRITEPATH . $this->database;
}
- return (! $this->password)
+ $sqlite = (! $this->password)
? new SQLite3($this->database)
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
+
+ $sqlite->enableExceptions(true);
+
+ return $sqlite;
} catch (Exception $e) {
throw new DatabaseException('SQLite3 error: ' . $e->getMessage());
}
@@ -144,7 +149,7 @@ protected function execute(string $sql)
return $this->isWriteType($sql)
? $this->connID->exec($sql)
: $this->connID->query($sql);
- } catch (ErrorException $e) {
+ } catch (Exception $e) {
log_message('error', (string) $e);
if ($this->DBDebug) {
diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php
index 4be650e03155..32e447829c29 100644
--- a/system/Database/SQLite3/Forge.php
+++ b/system/Database/SQLite3/Forge.php
@@ -1,5 +1,7 @@
db->escapeIdentifiers($processedField['name'])
. ' IN ' . $processedField['length'] . ')';
}
diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php
index e892e73bde10..21dc4c2fdeff 100644
--- a/system/Database/SQLite3/PreparedQuery.php
+++ b/system/Database/SQLite3/PreparedQuery.php
@@ -1,5 +1,7 @@
statement->bindValue($key + 1, $item, $bindType);
}
- $this->result = $this->statement->execute();
+ try {
+ $this->result = $this->statement->execute();
+ } catch (Exception $e) {
+ if ($this->db->DBDebug) {
+ throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return false;
+ }
return $this->result !== false;
}
diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php
index f3887b044b49..70d8dc47d2b6 100644
--- a/system/Database/SQLite3/Result.php
+++ b/system/Database/SQLite3/Result.php
@@ -1,5 +1,7 @@
db->DBPrefix;
- if (! empty($prefix) && strpos($table, $prefix) === 0) {
+ if (! empty($prefix) && str_starts_with($table, $prefix)) {
$table = substr($table, strlen($prefix));
}
@@ -430,7 +432,7 @@ protected function formatFields($fields)
*/
private function isIntegerType(string $type): bool
{
- return strpos(strtoupper($type), 'INT') !== false;
+ return str_contains(strtoupper($type), 'INT');
}
/**
diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php
index b8f45dd54196..93f7f0e00db7 100644
--- a/system/Database/SQLite3/Utils.php
+++ b/system/Database/SQLite3/Utils.php
@@ -1,5 +1,7 @@
seedPath . str_replace('.php', '', $class) . '.php';
if (! is_file($path)) {
diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php
index 6a5b5e47e5c7..dbccdcd07790 100644
--- a/system/Debug/BaseExceptionHandler.php
+++ b/system/Debug/BaseExceptionHandler.php
@@ -1,5 +1,7 @@
get_class($exception),
- 'type' => get_class($exception),
+ 'title' => $exception::class,
+ 'type' => $exception::class,
'code' => $statusCode,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
@@ -114,7 +116,7 @@ private function maskData($args, array $keysToMask, string $path = '')
$explode = explode('/', $keyToMask);
$index = end($explode);
- if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) {
+ if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
if (is_array($args) && array_key_exists($index, $args)) {
$args[$index] = '******************';
} elseif (
@@ -176,7 +178,7 @@ protected static function highlightFile(string $file, int $lineNumber, int $line
try {
$source = file_get_contents($file);
- } catch (Throwable $e) {
+ } catch (Throwable) {
return false;
}
diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php
index 09c6de0d5b7e..d6f97b76a32b 100644
--- a/system/Debug/ExceptionHandler.php
+++ b/system/Debug/ExceptionHandler.php
@@ -1,5 +1,7 @@
setStatusCode($statusCode);
- } catch (HTTPException $e) {
+ } catch (HTTPException) {
// Workaround for invalid HTTP status code.
$statusCode = 500;
$response->setStatusCode($statusCode);
@@ -73,7 +75,7 @@ public function handle(
);
}
- if (strpos($request->getHeaderLine('accept'), 'text/html') === false) {
+ if (! str_contains($request->getHeaderLine('accept'), 'text/html')) {
$data = (ENVIRONMENT === 'development' || ENVIRONMENT === 'testing')
? $this->collectVars($exception, $statusCode)
: '';
diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php
index bbfcb6ba70ab..d0379e2abd63 100644
--- a/system/Debug/ExceptionHandlerInterface.php
+++ b/system/Debug/ExceptionHandlerInterface.php
@@ -1,5 +1,7 @@
exceptionHandler(...));
+ set_error_handler($this->errorHandler(...));
register_shutdown_function([$this, 'shutdownHandler']);
}
@@ -124,12 +126,18 @@ public function exceptionHandler(Throwable $exception)
[$statusCode, $exitCode] = $this->determineCodes($exception);
+ $this->request = Services::request();
+
if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) {
- log_message('critical', get_class($exception) . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [
- 'message' => $exception->getMessage(),
- 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file
- 'exLine' => $exception->getLine(), // {line} refers to THIS line
- 'trace' => self::renderBacktrace($exception->getTrace()),
+ $uri = $this->request->getPath() === '' ? '/' : $this->request->getPath();
+ $routeInfo = '[Method: ' . $this->request->getMethod() . ', Route: ' . $uri . ']';
+
+ log_message('critical', $exception::class . ": {message}\n{routeInfo}\nin {exFile} on line {exLine}.\n{trace}", [
+ 'message' => $exception->getMessage(),
+ 'routeInfo' => $routeInfo,
+ 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file
+ 'exLine' => $exception->getLine(), // {line} refers to THIS line
+ 'trace' => self::renderBacktrace($exception->getTrace()),
]);
// Get the first exception.
@@ -138,7 +146,7 @@ public function exceptionHandler(Throwable $exception)
while ($prevException = $last->getPrevious()) {
$last = $prevException;
- log_message('critical', '[Caused by] ' . get_class($prevException) . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [
+ log_message('critical', '[Caused by] ' . $prevException::class . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [
'message' => $prevException->getMessage(),
'exFile' => clean_path($prevException->getFile()), // {file} refers to THIS file
'exLine' => $prevException->getLine(), // {line} refers to THIS line
@@ -147,7 +155,6 @@ public function exceptionHandler(Throwable $exception)
}
}
- $this->request = Services::request();
$this->response = Services::response();
if (method_exists($this->config, 'handler')) {
@@ -168,7 +175,7 @@ public function exceptionHandler(Throwable $exception)
if (! is_cli()) {
try {
$this->response->setStatusCode($statusCode);
- } catch (HTTPException $e) {
+ } catch (HTTPException) {
// Workaround for invalid HTTP status code.
$statusCode = 500;
$this->response->setStatusCode($statusCode);
@@ -178,7 +185,7 @@ public function exceptionHandler(Throwable $exception)
header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode);
}
- if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) {
+ if (! str_contains($this->request->getHeaderLine('accept'), 'text/html')) {
$this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();
exit($exitCode);
@@ -236,7 +243,7 @@ public function shutdownHandler()
if ($this->exceptionCaughtByExceptionHandler instanceof Throwable) {
$message .= "\n【Previous Exception】\n"
- . get_class($this->exceptionCaughtByExceptionHandler) . "\n"
+ . $this->exceptionCaughtByExceptionHandler::class . "\n"
. $this->exceptionCaughtByExceptionHandler->getMessage() . "\n"
. $this->exceptionCaughtByExceptionHandler->getTraceAsString();
}
@@ -348,8 +355,8 @@ protected function collectVars(Throwable $exception, int $statusCode): array
}
return [
- 'title' => get_class($exception),
- 'type' => get_class($exception),
+ 'title' => $exception::class,
+ 'type' => $exception::class,
'code' => $statusCode,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
@@ -389,7 +396,7 @@ private function maskData($args, array $keysToMask, string $path = '')
$explode = explode('/', $keyToMask);
$index = end($explode);
- if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) {
+ if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
if (is_array($args) && array_key_exists($index, $args)) {
$args[$index] = '******************';
} elseif (
@@ -473,25 +480,13 @@ private function handleDeprecationError(string $message, ?string $file = null, ?
*/
public static function cleanPath(string $file): string
{
- switch (true) {
- case strpos($file, APPPATH) === 0:
- $file = 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH));
- break;
-
- case strpos($file, SYSTEMPATH) === 0:
- $file = 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH));
- break;
-
- case strpos($file, FCPATH) === 0:
- $file = 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH));
- break;
-
- case defined('VENDORPATH') && strpos($file, VENDORPATH) === 0:
- $file = 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH));
- break;
- }
-
- return $file;
+ return match (true) {
+ str_starts_with($file, APPPATH) => 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)),
+ str_starts_with($file, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)),
+ str_starts_with($file, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)),
+ defined('VENDORPATH') && str_starts_with($file, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)),
+ default => $file,
+ };
}
/**
@@ -537,7 +532,7 @@ public static function highlightFile(string $file, int $lineNumber, int $lines =
try {
$source = file_get_contents($file);
- } catch (Throwable $e) {
+ } catch (Throwable) {
return false;
}
@@ -602,23 +597,12 @@ private static function renderBacktrace(array $backtrace): string
$idx = $index;
$idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT);
- $args = implode(', ', array_map(static function ($value): string {
- switch (true) {
- case is_object($value):
- return sprintf('Object(%s)', get_class($value));
-
- case is_array($value):
- return $value !== [] ? '[...]' : '[]';
-
- case $value === null:
- return 'null';
-
- case is_resource($value):
- return sprintf('resource (%s)', get_resource_type($value));
-
- default:
- return var_export($value, true);
- }
+ $args = implode(', ', array_map(static fn ($value): string => match (true) {
+ is_object($value) => sprintf('Object(%s)', $value::class),
+ is_array($value) => $value !== [] ? '[...]' : '[]',
+ $value === null => 'null',
+ is_resource($value) => sprintf('resource (%s)', get_resource_type($value)),
+ default => var_export($value, true),
}, $frame['args']));
$backtraces[] = sprintf(
diff --git a/system/Debug/Iterator.php b/system/Debug/Iterator.php
index 4bfb3a5bc88c..55ee0f3bce8b 100644
--- a/system/Debug/Iterator.php
+++ b/system/Debug/Iterator.php
@@ -1,5 +1,7 @@
getMethod());
+ $data['method'] = $request->getMethod();
$data['isAJAX'] = $request->isAJAX();
$data['startTime'] = $startTime;
$data['totalTime'] = $totalTime * 1000;
@@ -140,8 +143,17 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques
$data['vars']['post'][esc($name)] = is_array($value) ? '' . esc(print_r($value, true)) . '
' : esc($value);
}
- foreach ($request->headers() as $header) {
- $data['vars']['headers'][esc($header->getName())] = esc($header->getValueLine());
+ foreach ($request->headers() as $name => $value) {
+ if ($value instanceof Header) {
+ $data['vars']['headers'][esc($name)] = esc($value->getValueLine());
+ } else {
+ foreach ($value as $i => $header) {
+ $index = $i + 1;
+ $data['vars']['headers'][esc($name)] ??= '';
+ $data['vars']['headers'][esc($name)] .= ' (' . $index . ') '
+ . esc($header->getValueLine());
+ }
+ }
}
foreach ($request->getCookie() as $name => $value) {
@@ -157,8 +169,17 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques
'headers' => [],
];
- foreach ($response->headers() as $header) {
- $data['vars']['response']['headers'][esc($header->getName())] = esc($header->getValueLine());
+ foreach ($response->headers() as $name => $value) {
+ if ($value instanceof Header) {
+ $data['vars']['response']['headers'][esc($name)] = esc($value->getValueLine());
+ } else {
+ foreach ($value as $i => $header) {
+ $index = $i + 1;
+ $data['vars']['response']['headers'][esc($name)] ??= '';
+ $data['vars']['response']['headers'][esc($name)] .= ' (' . $index . ') '
+ . esc($header->getValueLine());
+ }
+ }
}
$data['config'] = Config::display();
@@ -354,10 +375,10 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r
* @var IncomingRequest|null $request
*/
if (CI_DEBUG && ! is_cli()) {
- $app = Services::codeigniter();
+ $app = service('codeigniter');
- $request ??= Services::request();
- $response ??= Services::response();
+ $request ??= service('request');
+ $response ??= service('response');
// Disable the toolbar for downloads
if ($response instanceof DownloadResponse) {
@@ -389,7 +410,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r
// Non-HTML formats should not include the debugbar
// then we send headers saying where to find the debug data
// for this response
- if ($request->isAJAX() || strpos($format, 'html') === false) {
+ if ($request->isAJAX() || ! str_contains($format, 'html')) {
$response->setHeader('Debugbar-Time', "{$time}")
->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}"));
@@ -412,7 +433,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r
. $kintScript
. PHP_EOL;
- if (strpos($response->getBody(), '') !== false) {
+ if (str_contains($response->getBody(), '')) {
$response->setBody(
preg_replace(
'//',
@@ -442,7 +463,7 @@ public function respond()
return;
}
- $request = Services::request();
+ $request = service('request');
// If the request contains '?debugbar then we're
// simply returning the loading script
@@ -491,7 +512,7 @@ protected function format(string $data, string $format = 'html'): string
{
$data = json_decode($data, true);
- if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) Services::request()->getGet('debugbar_time'), $debugbarTime)) {
+ if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) {
$history = new History();
$history->setFiles(
$debugbarTime[0],
diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php
index 4704fcdcaaeb..81cc631f98e7 100644
--- a/system/Debug/Toolbar/Collectors/BaseCollector.php
+++ b/system/Debug/Toolbar/Collectors/BaseCollector.php
@@ -1,5 +1,7 @@
ENVIRONMENT,
'baseURL' => $config->baseURL,
'timezone' => app_timezone(),
- 'locale' => Services::request()->getLocale(),
+ 'locale' => service('request')->getLocale(),
'cspEnabled' => $config->CSPEnabled,
];
}
diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php
index 5b5614ad3b9c..ee9f829b05b8 100644
--- a/system/Debug/Toolbar/Collectors/Database.php
+++ b/system/Debug/Toolbar/Collectors/Database.php
@@ -1,5 +1,7 @@
$path,
'name' => basename($file),
diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php
index fcf06e16f54a..4e5276b55b4e 100644
--- a/system/Debug/Toolbar/Collectors/History.php
+++ b/system/Debug/Toolbar/Collectors/History.php
@@ -1,5 +1,7 @@
controllerName(), $router->methodName());
- } catch (ReflectionException $e) {
- // If we're here, the method doesn't exist
- // and is likely calculated in _remap.
- $method = new ReflectionMethod($router->controllerName(), '_remap');
+ } catch (ReflectionException) {
+ try {
+ // If we're here, the method doesn't exist
+ // and is likely calculated in _remap.
+ $method = new ReflectionMethod($router->controllerName(), '_remap');
+ } catch (ReflectionException) {
+ // If we're here, page cache is returned. The router is not executed.
+ return [
+ 'matchedRoute' => [],
+ 'routes' => [],
+ ];
+ }
}
}
diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php
index cfce9e617594..163c9f5743f0 100644
--- a/system/Debug/Toolbar/Collectors/Timers.php
+++ b/system/Debug/Toolbar/Collectors/Timers.php
@@ -1,5 +1,7 @@
viewer ??= Services::renderer();
+ $this->viewer ??= service('renderer');
}
/**
diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php
index 03500456757e..8179c1a8e631 100644
--- a/system/Debug/Toolbar/Views/toolbar.tpl.php
+++ b/system/Debug/Toolbar/Views/toolbar.tpl.php
@@ -1,4 +1,4 @@
-setErrorMessage(lang('Email.attachmentMissing', [$file]));
return false;
@@ -748,7 +750,7 @@ public function setHeader($header, $value)
protected function stringToArray($email)
{
if (! is_array($email)) {
- return (strpos($email, ',') !== false) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email);
+ return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email);
}
return $email;
@@ -872,7 +874,7 @@ protected function getEncoding()
}
foreach ($this->baseCharsets as $charset) {
- if (strpos($this->charset, $charset) === 0) {
+ if (str_starts_with($this->charset, $charset)) {
$this->encoding = '7bit';
break;
@@ -907,7 +909,7 @@ protected function setDate()
{
$timezone = date('Z');
$operator = ($timezone[0] === '-') ? '-' : '+';
- $timezone = abs($timezone);
+ $timezone = abs((int) $timezone);
$timezone = floor($timezone / 3600) * 100 + ($timezone % 3600) / 60;
return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone);
@@ -1021,7 +1023,7 @@ public function wordWrap($str, $charlim = null)
$charlim = empty($this->wrapChars) ? 76 : $this->wrapChars;
}
- if (strpos($str, "\r") !== false) {
+ if (str_contains($str, "\r")) {
$str = str_replace(["\r\n", "\r"], "\n", $str);
}
@@ -1419,7 +1421,7 @@ protected function prepQuotedPrintable($str)
$str = preg_replace(['| +|', '/\x00+/'], [' ', ''], $str);
// Standardize newlines
- if (strpos($str, "\r") !== false) {
+ if (str_contains($str, "\r")) {
$str = str_replace(["\r\n", "\r"], "\n", $str);
}
@@ -1654,10 +1656,7 @@ protected function unwrapSpecials()
{
$this->finalBody = preg_replace_callback(
'/\{unwrap\}(.*?)\{\/unwrap\}/si',
- [
- $this,
- 'removeNLCallback',
- ],
+ $this->removeNLCallback(...),
$this->finalBody
);
}
@@ -1673,7 +1672,7 @@ protected function unwrapSpecials()
*/
protected function removeNLCallback($matches)
{
- if (strpos($matches[1], "\r") !== false || strpos($matches[1], "\n") !== false) {
+ if (str_contains($matches[1], "\r") || str_contains($matches[1], "\n")) {
$matches[1] = str_replace(["\r\n", "\r", "\n"], '', $matches[1]);
}
@@ -1854,7 +1853,7 @@ protected function sendWithSmtp()
$this->setErrorMessage($reply);
$this->SMTPEnd();
- if (strpos($reply, '250') !== 0) {
+ if (! str_starts_with($reply, '250')) {
$this->setErrorMessage(lang('Email.SMTPError', [$reply]));
return false;
@@ -2026,11 +2025,11 @@ protected function SMTPAuthenticate()
$this->sendData('AUTH LOGIN');
$reply = $this->getSMTPData();
- if (strpos($reply, '503') === 0) { // Already authenticated
+ if (str_starts_with($reply, '503')) { // Already authenticated
return true;
}
- if (strpos($reply, '334') !== 0) {
+ if (! str_starts_with($reply, '334')) {
$this->setErrorMessage(lang('Email.failedSMTPLogin', [$reply]));
return false;
@@ -2039,7 +2038,7 @@ protected function SMTPAuthenticate()
$this->sendData(base64_encode($this->SMTPUser));
$reply = $this->getSMTPData();
- if (strpos($reply, '334') !== 0) {
+ if (! str_starts_with($reply, '334')) {
$this->setErrorMessage(lang('Email.SMTPAuthUsername', [$reply]));
return false;
@@ -2048,7 +2047,7 @@ protected function SMTPAuthenticate()
$this->sendData(base64_encode($this->SMTPPass));
$reply = $this->getSMTPData();
- if (strpos($reply, '235') !== 0) {
+ if (! str_starts_with($reply, '235')) {
$this->setErrorMessage(lang('Email.SMTPAuthPassword', [$reply]));
return false;
diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php
index 161238177c49..2f40995041f3 100644
--- a/system/Encryption/EncrypterInterface.php
+++ b/system/Encryption/EncrypterInterface.php
@@ -1,5 +1,7 @@
[Entity] --- (2) --> [Database]
+ * [App Code] <-- (4) --- [Entity] <-- (3) --- [Database]
*/
interface CastInterface
{
/**
- * Get
+ * Takes a raw value from Entity, returns its value for PHP.
*
* @param array|bool|float|int|object|string|null $value Data
* @param array $params Additional param
@@ -27,7 +33,7 @@ interface CastInterface
public static function get($value, array $params = []);
/**
- * Set
+ * Takes a PHP value, returns its raw value for Entity.
*
* @param array|bool|float|int|object|string|null $value Data
* @param array $params Additional param
diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php
index 423300cdae6f..2d01ad79b0ae 100644
--- a/system/Entity/Cast/DatetimeCast.php
+++ b/system/Entity/Cast/DatetimeCast.php
@@ -1,5 +1,7 @@
dataCaster = new DataCaster(
+ array_merge($this->defaultCastHandlers, $this->castHandlers),
+ null,
+ null,
+ false
+ );
+
$this->syncOriginal();
$this->fill($data);
@@ -167,7 +181,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu
{
$this->_cast = $cast;
- $keys = array_filter(array_keys($this->attributes), static fn ($key) => strpos($key, '_') !== 0);
+ $keys = array_filter(array_keys($this->attributes), static fn ($key) => ! str_starts_with($key, '_'));
if (is_array($this->datamap)) {
$keys = array_unique(
@@ -347,8 +361,8 @@ protected function mutateDate($value)
/**
* Provides the ability to cast an item as a specific data type.
- * Add ? at the beginning of $type (i.e. ?string) to get NULL
- * instead of casting $value if $value === null
+ * Add ? at the beginning of the type (i.e. ?string) to get `null`
+ * instead of casting $value when $value is null.
*
* @param bool|float|int|string|null $value Attribute value
* @param string $attribute Attribute name
@@ -360,58 +374,10 @@ protected function mutateDate($value)
*/
protected function castAs($value, string $attribute, string $method = 'get')
{
- if (empty($this->casts[$attribute])) {
- return $value;
- }
-
- $type = $this->casts[$attribute];
-
- $isNullable = false;
-
- if (strpos($type, '?') === 0) {
- $isNullable = true;
-
- if ($value === null) {
- return null;
- }
-
- $type = substr($type, 1);
- }
-
- // In order not to create a separate handler for the
- // json-array type, we transform the required one.
- $type = $type === 'json-array' ? 'json[array]' : $type;
-
- if (! in_array($method, ['get', 'set'], true)) {
- throw CastException::forInvalidMethod($method);
- }
-
- $params = [];
-
- // Attempt to retrieve additional parameters if specified
- // type[param, param2,param3]
- if (preg_match('/^(.+)\[(.+)\]$/', $type, $matches)) {
- $type = $matches[1];
- $params = array_map('trim', explode(',', $matches[2]));
- }
-
- if ($isNullable) {
- $params[] = 'nullable';
- }
-
- $type = trim($type, '[]');
-
- $handlers = array_merge($this->defaultCastHandlers, $this->castHandlers);
-
- if (empty($handlers[$type])) {
- return $value;
- }
-
- if (! is_subclass_of($handlers[$type], CastInterface::class)) {
- throw CastException::forInvalidInterface($handlers[$type]);
- }
-
- return $handlers[$type]::$method($value, $params);
+ return $this->dataCaster
+ // @TODO if $casts is readonly, we don't need the setTypes() method.
+ ->setTypes($this->casts)
+ ->castAs($value, $attribute, $method);
}
/**
diff --git a/system/Entity/Exceptions/CastException.php b/system/Entity/Exceptions/CastException.php
index e259447b3f76..90d3885d9b62 100644
--- a/system/Entity/Exceptions/CastException.php
+++ b/system/Entity/Exceptions/CastException.php
@@ -1,5 +1,7 @@
new static(lang('Cast.jsonErrorDepth')),
+ JSON_ERROR_STATE_MISMATCH => new static(lang('Cast.jsonErrorStateMismatch')),
+ JSON_ERROR_CTRL_CHAR => new static(lang('Cast.jsonErrorCtrlChar')),
+ JSON_ERROR_SYNTAX => new static(lang('Cast.jsonErrorSyntax')),
+ JSON_ERROR_UTF8 => new static(lang('Cast.jsonErrorUtf8')),
+ default => new static(lang('Cast.jsonErrorUnknown')),
+ };
}
/**
diff --git a/system/Events/Events.php b/system/Events/Events.php
index f756fa0ec0c4..a06bd7903e69 100644
--- a/system/Events/Events.php
+++ b/system/Events/Events.php
@@ -1,5 +1,7 @@
shouldDiscover('events')) {
- $files = Services::locator()->search('Config/Events.php');
+ $files = service('locator')->search('Config/Events.php');
}
$files = array_filter(array_map(static function (string $file) {
diff --git a/system/Exceptions/CastException.php b/system/Exceptions/CastException.php
deleted file mode 100644
index 8dfb29543a78..000000000000
--- a/system/Exceptions/CastException.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
- *
- * For the full copyright and license information, please view
- * the LICENSE file that was distributed with this source code.
- */
-
-namespace CodeIgniter\Exceptions;
-
-/**
- * Cast Exceptions.
- *
- * @deprecated use CodeIgniter\Entity\Exceptions\CastException instead.
- *
- * @codeCoverageIgnore
- */
-class CastException extends CriticalError implements HasExitCodeInterface
-{
- use DebugTraceableTrait;
-
- public function getExitCode(): int
- {
- return EXIT_CONFIG;
- }
-
- /**
- * @return static
- */
- public static function forInvalidJsonFormatException(int $error)
- {
- switch ($error) {
- case JSON_ERROR_DEPTH:
- return new static(lang('Cast.jsonErrorDepth'));
-
- case JSON_ERROR_STATE_MISMATCH:
- return new static(lang('Cast.jsonErrorStateMismatch'));
-
- case JSON_ERROR_CTRL_CHAR:
- return new static(lang('Cast.jsonErrorCtrlChar'));
-
- case JSON_ERROR_SYNTAX:
- return new static(lang('Cast.jsonErrorSyntax'));
-
- case JSON_ERROR_UTF8:
- return new static(lang('Cast.jsonErrorUtf8'));
-
- default:
- return new static(lang('Cast.jsonErrorUnknown'));
- }
- }
-}
diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php
index 6eea2638c5e0..d8849b809c68 100644
--- a/system/Exceptions/ConfigException.php
+++ b/system/Exceptions/ConfigException.php
@@ -1,5 +1,7 @@
getSize() / 1024, 3);
-
- case 'mb':
- return number_format(($this->getSize() / 1024) / 1024, 3);
-
- default:
- return $this->getSize();
- }
+ return match (strtolower($unit)) {
+ 'kb' => number_format($this->getSize() / 1024, 3),
+ 'mb' => number_format(($this->getSize() / 1024) / 1024, 3),
+ default => $this->getSize(),
+ };
}
/**
@@ -173,7 +170,7 @@ public function getDestination(string $destination, string $delimiter = '_', int
$info = pathinfo($destination);
$extension = isset($info['extension']) ? '.' . $info['extension'] : '';
- if (strpos($info['filename'], $delimiter) !== false) {
+ if (str_contains($info['filename'], $delimiter)) {
$parts = explode($delimiter, $info['filename']);
if (is_numeric(end($parts))) {
diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php
index 8e608e3ab311..b9456dcc15ba 100644
--- a/system/Files/FileCollection.php
+++ b/system/Files/FileCollection.php
@@ -1,5 +1,7 @@
strpos($value, $directory) === 0);
+ return array_filter($files, static fn (string $value): bool => str_starts_with($value, $directory));
}
/**
@@ -180,7 +182,7 @@ public function add($paths, bool $recursive = true)
try {
// Test for a directory
self::resolveDirectory($path);
- } catch (FileException $e) {
+ } catch (FileException) {
$this->addFile($path);
continue;
diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php
index 5145b5b089c6..90ccb9b501a0 100644
--- a/system/Filters/CSRF.php
+++ b/system/Filters/CSRF.php
@@ -1,5 +1,7 @@
verify($request);
diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php
new file mode 100644
index 000000000000..93ca551b42f2
--- /dev/null
+++ b/system/Filters/Cors.php
@@ -0,0 +1,111 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Filters;
+
+use CodeIgniter\HTTP\Cors as CorsService;
+use CodeIgniter\HTTP\IncomingRequest;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+
+/**
+ * @see \CodeIgniter\Filters\CorsTest
+ */
+class Cors implements FilterInterface
+{
+ private ?CorsService $cors = null;
+
+ /**
+ * @testTag $config is used for testing purposes only.
+ *
+ * @param array{
+ * allowedOrigins?: list,
+ * allowedOriginsPatterns?: list,
+ * supportsCredentials?: bool,
+ * allowedHeaders?: list,
+ * exposedHeaders?: list,
+ * allowedMethods?: list,
+ * maxAge?: int,
+ * } $config
+ */
+ public function __construct(array $config = [])
+ {
+ if ($config !== []) {
+ $this->cors = new CorsService($config);
+ }
+ }
+
+ /**
+ * @param list|null $arguments
+ *
+ * @return ResponseInterface|string|void
+ */
+ public function before(RequestInterface $request, $arguments = null)
+ {
+ if (! $request instanceof IncomingRequest) {
+ return;
+ }
+
+ $this->createCorsService($arguments);
+
+ if (! $this->cors->isPreflightRequest($request)) {
+ return;
+ }
+
+ /** @var ResponseInterface $response */
+ $response = service('response');
+
+ $response = $this->cors->handlePreflightRequest($request, $response);
+
+ // Always adds `Vary: Access-Control-Request-Method` header for cacheability.
+ // If there is an intermediate cache server such as a CDN, if a plain
+ // OPTIONS request is sent, it may be cached. But valid preflight requests
+ // have this header, so it will be cached separately.
+ $response->appendHeader('Vary', 'Access-Control-Request-Method');
+
+ return $response;
+ }
+
+ /**
+ * @param list|null $arguments
+ */
+ private function createCorsService(?array $arguments): void
+ {
+ $this->cors ??= ($arguments === null) ? CorsService::factory()
+ : CorsService::factory($arguments[0]);
+ }
+
+ /**
+ * @param list|null $arguments
+ *
+ * @return ResponseInterface|void
+ */
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
+ {
+ if (! $request instanceof IncomingRequest) {
+ return;
+ }
+
+ $this->createCorsService($arguments);
+
+ // Always adds `Vary: Access-Control-Request-Method` header for cacheability.
+ // If there is an intermediate cache server such as a CDN, if a plain
+ // OPTIONS request is sent, it may be cached. But valid preflight requests
+ // have this header, so it will be cached separately.
+ if ($request->is('OPTIONS')) {
+ $response->appendHeader('Vary', 'Access-Control-Request-Method');
+ }
+
+ return $this->cors->addResponseHeaders($request, $response);
+ }
+}
diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php
index b05a92b6640e..9f864ee572c5 100644
--- a/system/Filters/DebugToolbar.php
+++ b/system/Filters/DebugToolbar.php
@@ -1,5 +1,7 @@
prepare($request, $response);
+ service('toolbar')->prepare($request, $response);
}
}
diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php
index 04bb36bdc2c5..1226ba318429 100644
--- a/system/Filters/Exceptions/FilterException.php
+++ b/system/Filters/Exceptions/FilterException.php
@@ -1,5 +1,7 @@
*/
protected $filters = [
@@ -77,6 +82,8 @@ class Filters
* The collection of filters' class names that will
* be used to execute in each position.
*
+ * This does not include "Required Filters".
+ *
* @var array
*/
protected $filtersClass = [
@@ -128,7 +135,7 @@ public function __construct($config, RequestInterface $request, ResponseInterfac
*/
private function discoverFilters(): void
{
- $locator = Services::locator();
+ $locator = service('locator');
// for access by custom filters
$filters = $this->config;
@@ -136,10 +143,11 @@ private function discoverFilters(): void
$files = $locator->search('Config/Filters.php');
foreach ($files as $file) {
+ // The $file may not be a class file.
$className = $locator->getClassname($file);
// Don't include our main Filter config again...
- if ($className === FiltersConfig::class) {
+ if ($className === FiltersConfig::class || $className === BaseFiltersConfig::class) {
continue;
}
@@ -161,7 +169,8 @@ public function setResponse(ResponseInterface $response)
* Runs through all of the filters for the specified
* uri and position.
*
- * @param string $uri URI path relative to baseURL
+ * @param string $uri URI path relative to baseURL
+ * @phpstan-param 'before'|'after' $position
*
* @return RequestInterface|ResponseInterface|string|null
*
@@ -171,55 +180,185 @@ public function run(string $uri, string $position = 'before')
{
$this->initialize(strtolower($uri));
- foreach ($this->filtersClass[$position] as $className) {
+ if ($position === 'before') {
+ return $this->runBefore($this->filtersClass[$position]);
+ }
+
+ // After
+ return $this->runAfter($this->filtersClass[$position]);
+ }
+
+ /**
+ * @return RequestInterface|ResponseInterface|string
+ */
+ private function runBefore(array $filterClasses)
+ {
+ foreach ($filterClasses as $className) {
$class = new $className();
if (! $class instanceof FilterInterface) {
- throw FilterException::forIncorrectInterface(get_class($class));
+ throw FilterException::forIncorrectInterface($class::class);
}
- if ($position === 'before') {
- $result = $class->before(
- $this->request,
- $this->argumentsClass[$className] ?? null
- );
-
- if ($result instanceof RequestInterface) {
- $this->request = $result;
+ $result = $class->before(
+ $this->request,
+ $this->argumentsClass[$className] ?? null
+ );
- continue;
- }
+ if ($result instanceof RequestInterface) {
+ $this->request = $result;
- // If the response object was sent back,
- // then send it and quit.
- if ($result instanceof ResponseInterface) {
- // short circuit - bypass any other filters
- return $result;
- }
- // Ignore an empty result
- if (empty($result)) {
- continue;
- }
+ continue;
+ }
+ // If the response object was sent back,
+ // then send it and quit.
+ if ($result instanceof ResponseInterface) {
+ // short circuit - bypass any other filters
return $result;
}
- if ($position === 'after') {
- $result = $class->after(
- $this->request,
- $this->response,
- $this->argumentsClass[$className] ?? null
- );
+ // Ignore an empty result
+ if (empty($result)) {
+ continue;
+ }
- if ($result instanceof ResponseInterface) {
- $this->response = $result;
+ return $result;
+ }
- continue;
- }
+ return $this->request;
+ }
+
+ private function runAfter(array $filterClasses): ResponseInterface
+ {
+ foreach ($filterClasses as $className) {
+ $class = new $className();
+
+ if (! $class instanceof FilterInterface) {
+ throw FilterException::forIncorrectInterface($class::class);
+ }
+
+ $result = $class->after(
+ $this->request,
+ $this->response,
+ $this->argumentsClass[$className] ?? null
+ );
+
+ if ($result instanceof ResponseInterface) {
+ $this->response = $result;
+
+ continue;
}
}
- return $position === 'before' ? $this->request : $this->response;
+ return $this->response;
+ }
+
+ /**
+ * Runs "Required Filters" for the specified position.
+ *
+ * @return RequestInterface|ResponseInterface|string|null
+ *
+ * @phpstan-param 'before'|'after' $position
+ *
+ * @throws FilterException
+ *
+ * @internal
+ */
+ public function runRequired(string $position = 'before')
+ {
+ [$filters, $aliases] = $this->getRequiredFilters($position);
+
+ if ($filters === []) {
+ return $position === 'before' ? $this->request : $this->response;
+ }
+
+ $filterClasses = [];
+
+ foreach ($filters as $alias) {
+ if (is_array($aliases[$alias])) {
+ $filterClasses[$position] = array_merge($filterClasses[$position], $aliases[$alias]);
+ } else {
+ $filterClasses[$position][] = $aliases[$alias];
+ }
+ }
+
+ if ($position === 'before') {
+ return $this->runBefore($filterClasses[$position]);
+ }
+
+ // After
+ return $this->runAfter($filterClasses[$position]);
+ }
+
+ /**
+ * Returns "Required Filters" for the specified position.
+ *
+ * @phpstan-param 'before'|'after' $position
+ *
+ * @internal
+ */
+ public function getRequiredFilters(string $position = 'before'): array
+ {
+ // For backward compatibility. For users who do not update Config\Filters.
+ if (! isset($this->config->required[$position])) {
+ $baseConfig = config(BaseFiltersConfig::class); // @phpstan-ignore-line
+ $filters = $baseConfig->required[$position];
+ $aliases = $baseConfig->aliases;
+ } else {
+ $filters = $this->config->required[$position];
+ $aliases = $this->config->aliases;
+ }
+
+ if ($filters === []) {
+ return [[], $aliases];
+ }
+
+ if ($position === 'after') {
+ if (in_array('toolbar', $this->filters['after'], true)) {
+ // It was already run in globals filters. So remove it.
+ $filters = $this->setToolbarToLast($filters, true);
+ } else {
+ // Set the toolbar filter to the last position to be executed
+ $filters = $this->setToolbarToLast($filters);
+ }
+ }
+
+ foreach ($filters as $alias) {
+ if (! array_key_exists($alias, $aliases)) {
+ throw FilterException::forNoAlias($alias);
+ }
+ }
+
+ return [$filters, $aliases];
+ }
+
+ /**
+ * Set the toolbar filter to the last position to be executed.
+ *
+ * @param list $filters `after` filter array
+ * @param bool $remove if true, remove `toolbar` filter
+ */
+ private function setToolbarToLast(array $filters, bool $remove = false): array
+ {
+ $afters = [];
+ $found = false;
+
+ foreach ($filters as $alias) {
+ if ($alias === 'toolbar') {
+ $found = true;
+
+ continue;
+ }
+
+ $afters[] = $alias;
+ }
+
+ if ($found && ! $remove) {
+ $afters[] = 'toolbar';
+ }
+
+ return $afters;
}
/**
@@ -237,6 +376,8 @@ public function run(string $uri, string $position = 'before')
*
* @param string|null $uri URI path relative to baseURL (all lowercase)
*
+ * @TODO We don't need to accept null as $uri.
+ *
* @return Filters
*/
public function initialize(?string $uri = null)
@@ -246,20 +387,21 @@ public function initialize(?string $uri = null)
}
// Decode URL-encoded string
- $uri = urldecode($uri);
-
- $this->processGlobals($uri);
- $this->processMethods();
- $this->processFilters($uri);
+ $uri = urldecode($uri ?? '');
+
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+ if ($oldFilterOrder) {
+ $this->processGlobals($uri);
+ $this->processMethods();
+ $this->processFilters($uri);
+ } else {
+ $this->processFilters($uri);
+ $this->processMethods();
+ $this->processGlobals($uri);
+ }
// Set the toolbar filter to the last position to be executed
- if (in_array('toolbar', $this->filters['after'], true)
- && ($count = count($this->filters['after'])) > 1
- && $this->filters['after'][$count - 1] !== 'toolbar'
- ) {
- array_splice($this->filters['after'], array_search('toolbar', $this->filters['after'], true), 1);
- $this->filters['after'][] = 'toolbar';
- }
+ $this->filters['after'] = $this->setToolbarToLast($this->filters['after']);
$this->processAliasesToClass('before');
$this->processAliasesToClass('after');
@@ -290,6 +432,7 @@ public function reset(): self
/**
* Returns the processed filters array.
+ * This does not include "Required Filters".
*/
public function getFilters(): array
{
@@ -298,6 +441,7 @@ public function getFilters(): array
/**
* Returns the filtersClass array.
+ * This does not include "Required Filters".
*/
public function getFiltersClass(): array
{
@@ -338,12 +482,8 @@ public function addFilter(string $class, ?string $alias = null, string $when = '
* are passed to the filter when executed.
*
* @param string $name filter_name or filter_name:arguments like 'role:admin,manager'
- *
- * @return $this
- *
- * @deprecated Use enableFilters(). This method will be private.
*/
- public function enableFilter(string $name, string $when = 'before')
+ private function enableFilter(string $name, string $when = 'before'): void
{
// Get arguments and clean name
[$name, $arguments] = $this->getCleanName($name);
@@ -365,8 +505,6 @@ public function enableFilter(string $name, string $when = 'before')
$this->filters[$when][] = $name;
$this->filtersClass[$when] = array_merge($this->filtersClass[$when], $classNames);
}
-
- return $this;
}
/**
@@ -380,7 +518,7 @@ private function getCleanName(string $name): array
{
$arguments = [];
- if (strpos($name, ':') !== false) {
+ if (str_contains($name, ':')) {
[$name, $arguments] = explode(':', $name);
$arguments = explode(',', $arguments);
@@ -444,6 +582,8 @@ protected function processGlobals(?string $uri = null)
// Add any global filters, unless they are excluded for this URI
$sets = ['before', 'after'];
+ $filters = [];
+
foreach ($sets as $set) {
if (isset($this->config->globals[$set])) {
// look at each alias in the group
@@ -463,11 +603,24 @@ protected function processGlobals(?string $uri = null)
}
if ($keep) {
- $this->filters[$set][] = $alias;
+ $filters[$set][] = $alias;
}
}
}
}
+
+ if (isset($filters['before'])) {
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+ if ($oldFilterOrder) {
+ $this->filters['before'] = array_merge($this->filters['before'], $filters['before']);
+ } else {
+ $this->filters['before'] = array_merge($filters['before'], $this->filters['before']);
+ }
+ }
+
+ if (isset($filters['after'])) {
+ $this->filters['after'] = array_merge($this->filters['after'], $filters['after']);
+ }
}
/**
@@ -481,11 +634,34 @@ protected function processMethods()
return;
}
- // Request method won't be set for CLI-based requests
- $method = strtolower($this->request->getMethod()) ?? 'cli';
+ $method = $this->request->getMethod();
+
+ $found = false;
if (array_key_exists($method, $this->config->methods)) {
- $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]);
+ $found = true;
+ }
+ // Checks lowercase HTTP method for backward compatibility.
+ // @deprecated 4.5.0
+ // @TODO remove this in the future.
+ elseif (array_key_exists(strtolower($method), $this->config->methods)) {
+ @trigger_error(
+ 'Setting lowercase HTTP method key "' . strtolower($method) . '" is deprecated.'
+ . ' Use uppercase HTTP method like "' . strtoupper($method) . '".',
+ E_USER_DEPRECATED
+ );
+
+ $found = true;
+ $method = strtolower($method);
+ }
+
+ if ($found) {
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+ if ($oldFilterOrder) {
+ $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]);
+ } else {
+ $this->filters['before'] = array_merge($this->config->methods[$method], $this->filters['before']);
+ }
}
}
@@ -505,6 +681,8 @@ protected function processFilters(?string $uri = null)
$uri = strtolower(trim($uri, '/ '));
// Add any filters that apply to this URI
+ $filters = [];
+
foreach ($this->config->filters as $alias => $settings) {
// Look for inclusion rules
if (isset($settings['before'])) {
@@ -514,7 +692,7 @@ protected function processFilters(?string $uri = null)
// Get arguments and clean name
[$name, $arguments] = $this->getCleanName($alias);
- $this->filters['before'][] = $name;
+ $filters['before'][] = $name;
$this->registerArguments($name, $arguments);
}
@@ -527,7 +705,7 @@ protected function processFilters(?string $uri = null)
// Get arguments and clean name
[$name, $arguments] = $this->getCleanName($alias);
- $this->filters['after'][] = $name;
+ $filters['after'][] = $name;
// The arguments may have already been registered in the before filter.
// So disable check.
@@ -535,6 +713,24 @@ protected function processFilters(?string $uri = null)
}
}
}
+
+ $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
+
+ if (isset($filters['before'])) {
+ if ($oldFilterOrder) {
+ $this->filters['before'] = array_merge($this->filters['before'], $filters['before']);
+ } else {
+ $this->filters['before'] = array_merge($filters['before'], $this->filters['before']);
+ }
+ }
+
+ if (isset($filters['after'])) {
+ if (! $oldFilterOrder) {
+ $filters['after'] = array_reverse($filters['after']);
+ }
+
+ $this->filters['after'] = array_merge($this->filters['after'], $filters['after']);
+ }
}
/**
@@ -571,6 +767,8 @@ private function registerArguments(string $name, array $arguments, bool $check =
*/
protected function processAliasesToClass(string $position)
{
+ $filterClasses = [];
+
foreach ($this->filters[$position] as $alias => $rules) {
if (is_numeric($alias) && is_string($rules)) {
$alias = $rules;
@@ -581,14 +779,20 @@ protected function processAliasesToClass(string $position)
}
if (is_array($this->config->aliases[$alias])) {
- $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $this->config->aliases[$alias]);
+ $filterClasses = [...$filterClasses, ...$this->config->aliases[$alias]];
} else {
- $this->filtersClass[$position][] = $this->config->aliases[$alias];
+ $filterClasses[] = $this->config->aliases[$alias];
}
}
- // when using enableFilter() we already write the class name in $filtersClass as well as the
+ // when using enableFilter() we already write the class name in $filterClasses as well as the
// alias in $filters. This leads to duplicates when using route filters.
+ if ($position === 'before') {
+ $this->filtersClass[$position] = array_merge($filterClasses, $this->filtersClass[$position]);
+ } else {
+ $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClasses);
+ }
+
// Since some filters like rate limiters rely on being executed once a request we filter em here.
$this->filtersClass[$position] = array_values(array_unique($this->filtersClass[$position]));
}
@@ -604,7 +808,7 @@ protected function processAliasesToClass(string $position)
private function pathApplies(string $uri, $paths)
{
// empty path matches all
- if (empty($paths)) {
+ if ($paths === '' || $paths === []) {
return true;
}
diff --git a/system/Filters/ForceHTTPS.php b/system/Filters/ForceHTTPS.php
new file mode 100644
index 000000000000..f415bd00e3b8
--- /dev/null
+++ b/system/Filters/ForceHTTPS.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Filters;
+
+use CodeIgniter\HTTP\Exceptions\RedirectException;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+use Config\App;
+
+/**
+ * Force HTTPS filter
+ */
+class ForceHTTPS implements FilterInterface
+{
+ /**
+ * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
+ * is true, will enforce that all requests to this site are made through
+ * HTTPS. Will redirect the user to the current page with HTTPS, as well
+ * as set the HTTP Strict Transport Security (HSTS) header for those browsers
+ * that support it.
+ *
+ * @param array|null $arguments
+ *
+ * @return ResponseInterface|void
+ */
+ public function before(RequestInterface $request, $arguments = null)
+ {
+ $config = config(App::class);
+
+ if ($config->forceGlobalSecureRequests !== true) {
+ return;
+ }
+
+ $response = service('response');
+
+ try {
+ force_https(YEAR, $request, $response);
+ } catch (RedirectException $e) {
+ return $e->getResponse();
+ }
+ }
+
+ /**
+ * We don't have anything to do here.
+ *
+ * @param array|null $arguments
+ *
+ * @return void
+ */
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
+ {
+ }
+}
diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php
index bd2dbf44920e..c2fb98c62d60 100644
--- a/system/Filters/Honeypot.php
+++ b/system/Filters/Honeypot.php
@@ -1,5 +1,7 @@
attachHoneypot($response);
+ service('honeypot')->attachHoneypot($response);
}
}
diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php
index 5528c880369e..542b12d7ffbe 100644
--- a/system/Filters/InvalidChars.php
+++ b/system/Filters/InvalidChars.php
@@ -1,5 +1,7 @@
checkEncoding(...), $value);
return $value;
}
@@ -112,7 +114,7 @@ protected function checkEncoding($value)
protected function checkControl($value)
{
if (is_array($value)) {
- array_map([$this, 'checkControl'], $value);
+ array_map($this->checkControl(...), $value);
return $value;
}
diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php
new file mode 100644
index 000000000000..a3d3af832a65
--- /dev/null
+++ b/system/Filters/PageCache.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Filters;
+
+use CodeIgniter\Cache\ResponseCache;
+use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\DownloadResponse;
+use CodeIgniter\HTTP\IncomingRequest;
+use CodeIgniter\HTTP\RedirectResponse;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+
+/**
+ * Page Cache filter
+ */
+class PageCache implements FilterInterface
+{
+ private readonly ResponseCache $pageCache;
+
+ public function __construct()
+ {
+ $this->pageCache = service('responsecache');
+ }
+
+ /**
+ * Checks page cache and return if found.
+ *
+ * @param array|null $arguments
+ *
+ * @return ResponseInterface|void
+ */
+ public function before(RequestInterface $request, $arguments = null)
+ {
+ assert($request instanceof CLIRequest || $request instanceof IncomingRequest);
+
+ $response = service('response');
+
+ $cachedResponse = $this->pageCache->get($request, $response);
+
+ if ($cachedResponse instanceof ResponseInterface) {
+ return $cachedResponse;
+ }
+ }
+
+ /**
+ * Cache the page.
+ *
+ * @param array|null $arguments
+ *
+ * @return void
+ */
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
+ {
+ assert($request instanceof CLIRequest || $request instanceof IncomingRequest);
+
+ if (
+ ! $response instanceof DownloadResponse
+ && ! $response instanceof RedirectResponse
+ ) {
+ // Cache it without the performance metrics replaced
+ // so that we can have live speed updates along the way.
+ // Must be run after filters to preserve the Response headers.
+ $this->pageCache->make($request, $response);
+ }
+ }
+}
diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php
new file mode 100644
index 000000000000..f2371c7a8d6d
--- /dev/null
+++ b/system/Filters/PerformanceMetrics.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Filters;
+
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+
+/**
+ * Performance Metrics filter
+ */
+class PerformanceMetrics implements FilterInterface
+{
+ /**
+ * We don't need to do anything here.
+ *
+ * @param array|null $arguments
+ */
+ public function before(RequestInterface $request, $arguments = null)
+ {
+ }
+
+ /**
+ * Replaces the performance metrics.
+ *
+ * @param array|null $arguments
+ *
+ * @return void
+ */
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
+ {
+ $body = $response->getBody();
+
+ if ($body !== null) {
+ $benchmark = service('timer');
+
+ $output = str_replace(
+ [
+ '{elapsed_time}',
+ '{memory_usage}',
+ ],
+ [
+ (string) $benchmark->getElapsedTime('total_execution'),
+ number_format(memory_get_peak_usage() / 1024 / 1024, 3),
+ ],
+ $body
+ );
+
+ $response->setBody($output);
+ }
+ }
+}
diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php
index cf557069193e..b8bd6e05f8eb 100644
--- a/system/Filters/SecureHeaders.php
+++ b/system/Filters/SecureHeaders.php
@@ -1,5 +1,7 @@
responseOrig = $response ?? new Response(config(App::class));
$this->baseURI = $uri->useRawQueryString();
@@ -130,7 +132,7 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response
* Sends an HTTP request to the specified $url. If this is a relative
* URL, it will be merged with $this->baseURI to form a complete URL.
*
- * @param string $method
+ * @param string $method HTTP method
*/
public function request($method, string $url, array $options = []): ResponseInterface
{
@@ -177,7 +179,7 @@ protected function resetOptions()
*/
public function get(string $url, array $options = []): ResponseInterface
{
- return $this->request('get', $url, $options);
+ return $this->request(Method::GET, $url, $options);
}
/**
@@ -185,7 +187,7 @@ public function get(string $url, array $options = []): ResponseInterface
*/
public function delete(string $url, array $options = []): ResponseInterface
{
- return $this->request('delete', $url, $options);
+ return $this->request('DELETE', $url, $options);
}
/**
@@ -193,7 +195,7 @@ public function delete(string $url, array $options = []): ResponseInterface
*/
public function head(string $url, array $options = []): ResponseInterface
{
- return $this->request('head', $url, $options);
+ return $this->request('HEAD', $url, $options);
}
/**
@@ -201,7 +203,7 @@ public function head(string $url, array $options = []): ResponseInterface
*/
public function options(string $url, array $options = []): ResponseInterface
{
- return $this->request('options', $url, $options);
+ return $this->request('OPTIONS', $url, $options);
}
/**
@@ -209,7 +211,7 @@ public function options(string $url, array $options = []): ResponseInterface
*/
public function patch(string $url, array $options = []): ResponseInterface
{
- return $this->request('patch', $url, $options);
+ return $this->request('PATCH', $url, $options);
}
/**
@@ -217,7 +219,7 @@ public function patch(string $url, array $options = []): ResponseInterface
*/
public function post(string $url, array $options = []): ResponseInterface
{
- return $this->request('post', $url, $options);
+ return $this->request(Method::POST, $url, $options);
}
/**
@@ -225,7 +227,7 @@ public function post(string $url, array $options = []): ResponseInterface
*/
public function put(string $url, array $options = []): ResponseInterface
{
- return $this->request('put', $url, $options);
+ return $this->request(Method::PUT, $url, $options);
}
/**
@@ -323,7 +325,7 @@ protected function parseOptions(array $options)
protected function prepareURL(string $url): string
{
// If it's a full URI, then we have nothing to do here...
- if (strpos($url, '://') !== false) {
+ if (str_contains($url, '://')) {
return $url;
}
@@ -339,17 +341,6 @@ protected function prepareURL(string $url): string
);
}
- /**
- * Get the request method. Overrides the Request class' method
- * since users expect a different answer here.
- *
- * @param bool|false $upper Whether to return in upper or lower case.
- */
- public function getMethod(bool $upper = false): string
- {
- return ($upper) ? strtoupper($this->method) : strtolower($this->method);
- }
-
/**
* Fires the actual cURL request.
*
@@ -389,16 +380,16 @@ public function send(string $method, string $url)
// Set the string we want to break our response from
$breakString = "\r\n\r\n";
- while (strpos($output, 'HTTP/1.1 100 Continue') === 0) {
+ while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
$output = substr($output, strpos($output, $breakString) + 4);
}
- if (strpos($output, 'HTTP/1.1 200 Connection established') === 0) {
+ if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) {
$output = substr($output, strpos($output, $breakString) + 4);
}
// If request and response have Digest
- if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && strpos($output, 'WWW-Authenticate: Digest') !== false) {
+ if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
$output = substr($output, strpos($output, $breakString) + 4);
}
@@ -446,8 +437,6 @@ protected function applyRequestHeaders(array $curlOptions = []): array
*/
protected function applyMethod(string $method, array $curlOptions): array
{
- $method = strtoupper($method);
-
$this->method = $method;
$curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
@@ -458,7 +447,7 @@ protected function applyMethod(string $method, array $curlOptions): array
return $this->applyBody($curlOptions);
}
- if ($method === 'PUT' || $method === 'POST') {
+ if ($method === Method::PUT || $method === Method::POST) {
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
$this->setHeader('Content-Length', '0');
@@ -492,11 +481,15 @@ protected function setResponseHeaders(array $headers = [])
{
foreach ($headers as $header) {
if (($pos = strpos($header, ':')) !== false) {
- $title = substr($header, 0, $pos);
- $value = substr($header, $pos + 1);
+ $title = trim(substr($header, 0, $pos));
+ $value = trim(substr($header, $pos + 1));
- $this->response->setHeader($title, $value);
- } elseif (strpos($header, 'HTTP') === 0) {
+ if ($this->response instanceof Response) {
+ $this->response->addHeader($title, $value);
+ } else {
+ $this->response->setHeader($title, $value);
+ }
+ } elseif (str_starts_with($header, 'HTTP')) {
preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
if (isset($matches[1])) {
diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php
index 83603b3a4a81..945c3e08d3ce 100644
--- a/system/HTTP/ContentSecurityPolicy.php
+++ b/system/HTTP/ContentSecurityPolicy.php
@@ -1,5 +1,7 @@
+ */
+ protected array $directives = [
+ 'base-uri' => 'baseURI',
+ 'child-src' => 'childSrc',
+ 'connect-src' => 'connectSrc',
+ 'default-src' => 'defaultSrc',
+ 'font-src' => 'fontSrc',
+ 'form-action' => 'formAction',
+ 'frame-ancestors' => 'frameAncestors',
+ 'frame-src' => 'frameSrc',
+ 'img-src' => 'imageSrc',
+ 'media-src' => 'mediaSrc',
+ 'object-src' => 'objectSrc',
+ 'plugin-types' => 'pluginTypes',
+ 'script-src' => 'scriptSrc',
+ 'style-src' => 'styleSrc',
+ 'manifest-src' => 'manifestSrc',
+ 'sandbox' => 'sandbox',
+ 'report-uri' => 'reportURI',
+ ];
+
/**
* Used for security enforcement
*
@@ -113,37 +140,37 @@ class ContentSecurityPolicy
/**
* Used for security enforcement
*
- * @var string
+ * @var array|string
*/
- protected $reportURI;
+ protected $scriptSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
- protected $sandbox = [];
+ protected $styleSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
- protected $scriptSrc = [];
+ protected $manifestSrc = [];
/**
* Used for security enforcement
*
* @var array|string
*/
- protected $styleSrc = [];
+ protected $sandbox = [];
/**
* Used for security enforcement
*
- * @var array|string
+ * @var string|null
*/
- protected $manifestSrc = [];
+ protected $reportURI;
/**
* Used for security enforcement
@@ -704,26 +731,6 @@ protected function buildHeaders(ResponseInterface $response)
$response->setHeader('Content-Security-Policy', []);
$response->setHeader('Content-Security-Policy-Report-Only', []);
- $directives = [
- 'base-uri' => 'baseURI',
- 'child-src' => 'childSrc',
- 'connect-src' => 'connectSrc',
- 'default-src' => 'defaultSrc',
- 'font-src' => 'fontSrc',
- 'form-action' => 'formAction',
- 'frame-ancestors' => 'frameAncestors',
- 'frame-src' => 'frameSrc',
- 'img-src' => 'imageSrc',
- 'media-src' => 'mediaSrc',
- 'object-src' => 'objectSrc',
- 'plugin-types' => 'pluginTypes',
- 'script-src' => 'scriptSrc',
- 'style-src' => 'styleSrc',
- 'manifest-src' => 'manifestSrc',
- 'sandbox' => 'sandbox',
- 'report-uri' => 'reportURI',
- ];
-
// inject default base & default URIs if needed
if (empty($this->baseURI)) {
$this->baseURI = 'self';
@@ -733,7 +740,7 @@ protected function buildHeaders(ResponseInterface $response)
$this->defaultSrc = 'self';
}
- foreach ($directives as $name => $property) {
+ foreach ($this->directives as $name => $property) {
if (! empty($this->{$property})) {
$this->addToHeader($name, $this->{$property});
}
@@ -795,7 +802,7 @@ protected function addToHeader(string $name, $values = null)
$reportOnly = $this->reportOnly;
}
- if (strpos($value, 'nonce-') === 0) {
+ if (str_starts_with($value, 'nonce-')) {
$value = "'{$value}'";
}
@@ -814,4 +821,20 @@ protected function addToHeader(string $name, $values = null)
$this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
}
}
+
+ /**
+ * Clear the directive.
+ *
+ * @param string $directive CSP directive
+ */
+ public function clearDirective(string $directive): void
+ {
+ if ($directive === 'report-uris') {
+ $this->{$this->directives[$directive]} = null;
+
+ return;
+ }
+
+ $this->{$this->directives[$directive]} = [];
+ }
}
diff --git a/system/HTTP/Cors.php b/system/HTTP/Cors.php
new file mode 100644
index 000000000000..f7619c9f83a4
--- /dev/null
+++ b/system/HTTP/Cors.php
@@ -0,0 +1,230 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\HTTP;
+
+use CodeIgniter\Exceptions\ConfigException;
+use Config\Cors as CorsConfig;
+
+/**
+ * Cross-Origin Resource Sharing (CORS)
+ *
+ * @see \CodeIgniter\HTTP\CorsTest
+ */
+class Cors
+{
+ /**
+ * @var array{
+ * allowedOrigins: list,
+ * allowedOriginsPatterns: list,
+ * supportsCredentials: bool,
+ * allowedHeaders: list,
+ * exposedHeaders: list,
+ * allowedMethods: list,
+ * maxAge: int,
+ * }
+ */
+ private array $config = [
+ 'allowedOrigins' => [],
+ 'allowedOriginsPatterns' => [],
+ 'supportsCredentials' => false,
+ 'allowedHeaders' => [],
+ 'exposedHeaders' => [],
+ 'allowedMethods' => [],
+ 'maxAge' => 7200,
+ ];
+
+ /**
+ * @param array{
+ * allowedOrigins?: list,
+ * allowedOriginsPatterns?: list,
+ * supportsCredentials?: bool,
+ * allowedHeaders?: list,
+ * exposedHeaders?: list,
+ * allowedMethods?: list,
+ * maxAge?: int,
+ * }|CorsConfig|null $config
+ */
+ public function __construct($config = null)
+ {
+ $config ??= config(CorsConfig::class);
+ if ($config instanceof CorsConfig) {
+ $config = $config->default;
+ }
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * Creates a new instance by config name.
+ */
+ public static function factory(string $configName = 'default'): self
+ {
+ $config = config(CorsConfig::class)->{$configName};
+
+ return new self($config);
+ }
+
+ /**
+ * Whether if the request is a preflight request.
+ */
+ public function isPreflightRequest(IncomingRequest $request): bool
+ {
+ return $request->is('OPTIONS')
+ && $request->hasHeader('Access-Control-Request-Method');
+ }
+
+ /**
+ * Handles the preflight request, and returns the response.
+ */
+ public function handlePreflightRequest(RequestInterface $request, ResponseInterface $response): ResponseInterface
+ {
+ $response->setStatusCode(204);
+
+ $this->setAllowOrigin($request, $response);
+
+ if ($response->hasHeader('Access-Control-Allow-Origin')) {
+ $this->setAllowHeaders($response);
+ $this->setAllowMethods($response);
+ $this->setAllowMaxAge($response);
+ $this->setAllowCredentials($response);
+ }
+
+ return $response;
+ }
+
+ private function checkWildcard(string $name, int $count): void
+ {
+ if (in_array('*', $this->config[$name], true) && $count > 1) {
+ throw new ConfigException(
+ "If wildcard is specified, you must set `'{$name}' => ['*']`."
+ . ' But using wildcard is not recommended.'
+ );
+ }
+ }
+
+ private function checkWildcardAndCredentials(string $name, string $header): void
+ {
+ if (
+ $this->config[$name] === ['*']
+ && $this->config['supportsCredentials']
+ ) {
+ throw new ConfigException(
+ 'When responding to a credentialed request, '
+ . 'the server must not specify the "*" wildcard for the '
+ . $header . ' response-header value.'
+ );
+ }
+ }
+
+ private function setAllowOrigin(RequestInterface $request, ResponseInterface $response): void
+ {
+ $originCount = count($this->config['allowedOrigins']);
+ $originPatternCount = count($this->config['allowedOriginsPatterns']);
+
+ $this->checkWildcard('allowedOrigins', $originCount);
+ $this->checkWildcardAndCredentials('allowedOrigins', 'Access-Control-Allow-Origin');
+
+ // Single Origin.
+ if ($originCount === 1 && $originPatternCount === 0) {
+ $response->setHeader('Access-Control-Allow-Origin', $this->config['allowedOrigins'][0]);
+
+ return;
+ }
+
+ // Multiple Origins.
+ if (! $request->hasHeader('Origin')) {
+ return;
+ }
+
+ $origin = $request->getHeaderLine('Origin');
+
+ if ($originCount > 1 && in_array($origin, $this->config['allowedOrigins'], true)) {
+ $response->setHeader('Access-Control-Allow-Origin', $origin);
+ $response->appendHeader('Vary', 'Origin');
+
+ return;
+ }
+
+ if ($originPatternCount > 0) {
+ foreach ($this->config['allowedOriginsPatterns'] as $pattern) {
+ $regex = '#\A' . $pattern . '\z#';
+
+ if (preg_match($regex, $origin)) {
+ $response->setHeader('Access-Control-Allow-Origin', $origin);
+ $response->appendHeader('Vary', 'Origin');
+
+ return;
+ }
+ }
+ }
+ }
+
+ private function setAllowHeaders(ResponseInterface $response): void
+ {
+ $this->checkWildcard('allowedHeaders', count($this->config['allowedHeaders']));
+ $this->checkWildcardAndCredentials('allowedHeaders', 'Access-Control-Allow-Headers');
+
+ $response->setHeader(
+ 'Access-Control-Allow-Headers',
+ implode(', ', $this->config['allowedHeaders'])
+ );
+ }
+
+ private function setAllowMethods(ResponseInterface $response): void
+ {
+ $this->checkWildcard('allowedMethods', count($this->config['allowedMethods']));
+ $this->checkWildcardAndCredentials('allowedMethods', 'Access-Control-Allow-Methods');
+
+ $response->setHeader(
+ 'Access-Control-Allow-Methods',
+ implode(', ', $this->config['allowedMethods'])
+ );
+ }
+
+ private function setAllowMaxAge(ResponseInterface $response): void
+ {
+ $response->setHeader('Access-Control-Max-Age', (string) $this->config['maxAge']);
+ }
+
+ private function setAllowCredentials(ResponseInterface $response): void
+ {
+ if ($this->config['supportsCredentials']) {
+ $response->setHeader('Access-Control-Allow-Credentials', 'true');
+ }
+ }
+
+ /**
+ * Adds CORS headers to the Response.
+ */
+ public function addResponseHeaders(RequestInterface $request, ResponseInterface $response): ResponseInterface
+ {
+ $this->setAllowOrigin($request, $response);
+
+ if ($response->hasHeader('Access-Control-Allow-Origin')) {
+ $this->setAllowCredentials($response);
+ $this->setExposeHeaders($response);
+ }
+
+ return $response;
+ }
+
+ private function setExposeHeaders(ResponseInterface $response): void
+ {
+ if ($this->config['exposedHeaders'] !== []) {
+ $response->setHeader(
+ 'Access-Control-Expose-Headers',
+ implode(', ', $this->config['exposedHeaders'])
+ );
+ }
+ }
+}
diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php
index d815a9af6e03..6b293fbca239 100644
--- a/system/HTTP/DownloadResponse.php
+++ b/system/HTTP/DownloadResponse.php
@@ -1,5 +1,7 @@
response) {
- $this->response = Services::response()
+ $this->response = service('response')
->redirect(base_url($this->getMessage()), 'auto', $this->getCode());
}
- Services::logger()->info(
+ service('logger')->info(
'REDIRECTED ROUTE at '
- . ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6))
+ . ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6))
);
return $this->response;
diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php
index 2ce05e2c3a4c..079d5e73114f 100644
--- a/system/HTTP/Files/FileCollection.php
+++ b/system/HTTP/Files/FileCollection.php
@@ -1,5 +1,7 @@
populateFiles();
if ($this->hasFile($name)) {
- if (strpos($name, '.') !== false) {
+ if (str_contains($name, '.')) {
$name = explode('.', $name);
$uploadedFile = $this->getValueDotNotationSyntax($name, $this->files);
@@ -84,7 +86,7 @@ public function getFileMultiple(string $name)
$this->populateFiles();
if ($this->hasFile($name)) {
- if (strpos($name, '.') !== false) {
+ if (str_contains($name, '.')) {
$name = explode('.', $name);
$uploadedFile = $this->getValueDotNotationSyntax($name, $this->files);
@@ -113,7 +115,7 @@ public function hasFile(string $fileID): bool
{
$this->populateFiles();
- if (strpos($fileID, '.') !== false) {
+ if (str_contains($fileID, '.')) {
$segments = explode('.', $fileID);
$el = $this->files;
@@ -185,7 +187,7 @@ protected function createFileObject(array $array)
$array['tmp_name'] ?? null,
$array['name'] ?? null,
$array['type'] ?? null,
- $array['size'] ?? null,
+ ($array['size'] ?? null) === null ? null : (int) $array['size'],
$array['error'] ?? null,
$array['full_path'] ?? null
);
diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php
index e0725e7c90d5..78643aa7166e 100644
--- a/system/HTTP/Files/UploadedFile.php
+++ b/system/HTTP/Files/UploadedFile.php
@@ -1,5 +1,7 @@
hasMoved = move_uploaded_file($this->path, $destination);
- } catch (Exception $e) {
+ } catch (Exception) {
$error = error_get_last();
$message = strip_tags($error['message'] ?? '');
diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php
index ef2073f2b876..12cc25a0d050 100644
--- a/system/HTTP/Files/UploadedFileInterface.php
+++ b/system/HTTP/Files/UploadedFileInterface.php
@@ -1,5 +1,7 @@
|string>|string
*/
diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php
index 499fa0b1cf06..ec13e40f6ff5 100755
--- a/system/HTTP/IncomingRequest.php
+++ b/system/HTTP/IncomingRequest.php
@@ -1,5 +1,7 @@
getHeaderLine('Content-Type'), 'multipart/form-data') === false
+ && ! str_contains($this->getHeaderLine('Content-Type'), 'multipart/form-data')
&& (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize()
) {
// Get our body from php://input
@@ -174,7 +153,6 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U
$body = null;
}
- $this->config = $config;
$this->uri = $uri;
$this->body = $body;
$this->userAgent = $userAgent;
@@ -195,24 +173,12 @@ private function getPostMaxSize(): int
{
$postMaxSize = ini_get('post_max_size');
- switch (strtoupper(substr($postMaxSize, -1))) {
- case 'G':
- $postMaxSize = (int) str_replace('G', '', $postMaxSize) * 1024 ** 3;
- break;
-
- case 'M':
- $postMaxSize = (int) str_replace('M', '', $postMaxSize) * 1024 ** 2;
- break;
-
- case 'K':
- $postMaxSize = (int) str_replace('K', '', $postMaxSize) * 1024;
- break;
-
- default:
- $postMaxSize = (int) $postMaxSize;
- }
-
- return $postMaxSize;
+ return match (strtoupper(substr($postMaxSize, -1))) {
+ 'G' => (int) str_replace('G', '', $postMaxSize) * 1024 ** 3,
+ 'M' => (int) str_replace('M', '', $postMaxSize) * 1024 ** 2,
+ 'K' => (int) str_replace('K', '', $postMaxSize) * 1024,
+ default => (int) $postMaxSize,
+ };
}
/**
@@ -260,20 +226,11 @@ public function detectPath(string $protocol = ''): string
$protocol = 'REQUEST_URI';
}
- switch ($protocol) {
- case 'REQUEST_URI':
- $this->path = $this->parseRequestURI();
- break;
-
- case 'QUERY_STRING':
- $this->path = $this->parseQueryString();
- break;
-
- case 'PATH_INFO':
- default:
- $this->path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI();
- break;
- }
+ $this->path = match ($protocol) {
+ 'REQUEST_URI' => $this->parseRequestURI(),
+ 'QUERY_STRING' => $this->parseQueryString(),
+ default => $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(),
+ };
return $this->path;
}
@@ -321,7 +278,7 @@ protected function parseRequestURI(): string
// This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct
// URI is found, and also fixes the QUERY_STRING Server var and $_GET array.
- if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) {
+ if (trim($uri, '/') === '' && str_starts_with($query, '/')) {
$query = explode('?', $query, 2);
$uri = $query[0];
$_SERVER['QUERY_STRING'] = $query[1] ?? '';
@@ -354,7 +311,7 @@ protected function parseQueryString(): string
return '/';
}
- if (strncmp($uri, '/', 1) === 0) {
+ if (str_starts_with($uri, '/')) {
$uri = explode('?', $uri, 2);
$_SERVER['QUERY_STRING'] = $uri[1] ?? '';
$uri = $uri[0];
@@ -380,21 +337,13 @@ public function negotiate(string $type, array $supported, bool $strictMatch = fa
$this->negotiator = Services::negotiator($this, true);
}
- switch (strtolower($type)) {
- case 'media':
- return $this->negotiator->media($supported, $strictMatch);
-
- case 'charset':
- return $this->negotiator->charset($supported);
-
- case 'encoding':
- return $this->negotiator->encoding($supported);
-
- case 'language':
- return $this->negotiator->language($supported);
- }
-
- throw HTTPException::forInvalidNegotiationType($type);
+ return match (strtolower($type)) {
+ 'media' => $this->negotiator->media($supported, $strictMatch),
+ 'charset' => $this->negotiator->charset($supported),
+ 'encoding' => $this->negotiator->encoding($supported),
+ 'language' => $this->negotiator->language($supported),
+ default => throw HTTPException::forInvalidNegotiationType($type),
+ };
}
/**
@@ -407,14 +356,14 @@ public function is(string $type): bool
{
$valueUpper = strtoupper($type);
- $httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS'];
+ $httpMethods = Method::all();
if (in_array($valueUpper, $httpMethods, true)) {
- return strtoupper($this->getMethod()) === $valueUpper;
+ return $this->getMethod() === $valueUpper;
}
if ($valueUpper === 'JSON') {
- return strpos($this->getHeaderLine('Content-Type'), 'application/json') !== false;
+ return str_contains($this->getHeaderLine('Content-Type'), 'application/json');
}
if ($valueUpper === 'AJAX') {
@@ -550,7 +499,7 @@ public function getDefaultLocale(): string
public function getVar($index = null, $filter = null, $flags = null)
{
if (
- strpos($this->getHeaderLine('Content-Type'), 'application/json') !== false
+ str_contains($this->getHeaderLine('Content-Type'), 'application/json')
&& $this->body !== null
) {
return $this->getJsonVar($index, false, $filter, $flags);
@@ -931,18 +880,4 @@ public function getFile(string $fileID)
return $this->files->getFile($fileID);
}
-
- /**
- * Remove relative directory (../) and multi slashes (///)
- *
- * Do some final cleaning of the URI and return it, currently only used in static::_parse_request_uri()
- *
- * @deprecated 4.1.2 Use URI::removeDotSegments() directly
- */
- protected function removeRelativeDirectory(string $uri): string
- {
- $uri = URI::removeDotSegments($uri);
-
- return $uri === '/' ? $uri : ltrim($uri, '/');
- }
}
diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php
index 9d0b517778a4..71c4429f28ee 100644
--- a/system/HTTP/Message.php
+++ b/system/HTTP/Message.php
@@ -1,5 +1,7 @@
hasMultipleHeaders($name)) {
+ throw new InvalidArgumentException(
+ 'The header "' . $name . '" already has multiple headers.'
+ . ' You cannot use getHeaderLine().'
+ );
+ }
+
$origName = $this->getHeaderName($name);
if (! array_key_exists($origName, $this->headers)) {
diff --git a/system/HTTP/MessageInterface.php b/system/HTTP/MessageInterface.php
index 99867bde5800..b8850a0ffb7c 100644
--- a/system/HTTP/MessageInterface.php
+++ b/system/HTTP/MessageInterface.php
@@ -1,5 +1,7 @@
An array of the Header objects
+ * @return array> An array of the Header objects
*/
public function headers(): array;
@@ -83,7 +85,7 @@ public function hasHeader(string $name): bool;
*
* @param string $name
*
- * @return array|Header|null
+ * @return Header|list|null
*/
public function header($name);
diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php
index ac3e5b18938c..2f6e57a90c57 100644
--- a/system/HTTP/MessageTrait.php
+++ b/system/HTTP/MessageTrait.php
@@ -1,5 +1,7 @@
+ * [name => Header]
+ * or
+ * [name => [Header1, Header2]]
+ *
+ * @var array>
*/
protected $headers = [];
@@ -93,7 +100,7 @@ public function populateHeaders(): void
$this->setHeader($header, $_SERVER[$key]);
- // Add us to the header map so we can find them case-insensitively
+ // Add us to the header map, so we can find them case-insensitively
$this->headerMap[strtolower($header)] = $header;
}
}
@@ -102,7 +109,7 @@ public function populateHeaders(): void
/**
* Returns an array containing all Headers.
*
- * @return array An array of the Header objects
+ * @return array> An array of the Header objects
*/
public function headers(): array
{
@@ -122,7 +129,7 @@ public function headers(): array
*
* @param string $name
*
- * @return array|Header|null
+ * @return Header|list|null
*/
public function header($name)
{
@@ -140,9 +147,14 @@ public function header($name)
*/
public function setHeader(string $name, $value): self
{
+ $this->checkMultipleHeaders($name);
+
$origName = $this->getHeaderName($name);
- if (isset($this->headers[$origName]) && is_array($this->headers[$origName]->getValue())) {
+ if (
+ isset($this->headers[$origName])
+ && is_array($this->headers[$origName]->getValue())
+ ) {
if (! is_array($value)) {
$value = [$value];
}
@@ -158,6 +170,23 @@ public function setHeader(string $name, $value): self
return $this;
}
+ private function hasMultipleHeaders(string $name): bool
+ {
+ $origName = $this->getHeaderName($name);
+
+ return isset($this->headers[$origName]) && is_array($this->headers[$origName]);
+ }
+
+ private function checkMultipleHeaders(string $name): void
+ {
+ if ($this->hasMultipleHeaders($name)) {
+ throw new InvalidArgumentException(
+ 'The header "' . $name . '" already has multiple headers.'
+ . ' You cannot change them. If you really need to change, remove the header first.'
+ );
+ }
+ }
+
/**
* Removes a header from the list of headers we track.
*
@@ -179,6 +208,8 @@ public function removeHeader(string $name): self
*/
public function appendHeader(string $name, ?string $value): self
{
+ $this->checkMultipleHeaders($name);
+
$origName = $this->getHeaderName($name);
array_key_exists($origName, $this->headers)
@@ -188,6 +219,33 @@ public function appendHeader(string $name, ?string $value): self
return $this;
}
+ /**
+ * Adds a header (not a header value) with the same name.
+ * Use this only when you set multiple headers with the same name,
+ * typically, for `Set-Cookie`.
+ *
+ * @return $this
+ */
+ public function addHeader(string $name, string $value): static
+ {
+ $origName = $this->getHeaderName($name);
+
+ if (! isset($this->headers[$origName])) {
+ $this->setHeader($name, $value);
+
+ return $this;
+ }
+
+ if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) {
+ $this->headers[$origName] = [$this->headers[$origName]];
+ }
+
+ // Add the header.
+ $this->headers[$origName][] = new Header($origName, $value);
+
+ return $this;
+ }
+
/**
* Adds an additional header value to any headers that accept
* multiple values (i.e. are an array or implement ArrayAccess)
@@ -196,6 +254,8 @@ public function appendHeader(string $name, ?string $value): self
*/
public function prependHeader(string $name, string $value): self
{
+ $this->checkMultipleHeaders($name);
+
$origName = $this->getHeaderName($name);
$this->headers[$origName]->prependValue($value);
diff --git a/system/HTTP/Method.php b/system/HTTP/Method.php
new file mode 100644
index 000000000000..ee3a09ec4b0d
--- /dev/null
+++ b/system/HTTP/Method.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\HTTP;
+
+/**
+ * HTTP Method List
+ */
+class Method
+{
+ /**
+ * Safe: No
+ * Idempotent: No
+ * Cacheable: No
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT
+ */
+ public const CONNECT = 'CONNECT';
+
+ /**
+ * Safe: No
+ * Idempotent: Yes
+ * Cacheable: No
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE
+ */
+ public const DELETE = 'DELETE';
+
+ /**
+ * Safe: Yes
+ * Idempotent: Yes
+ * Cacheable: Yes
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET
+ */
+ public const GET = 'GET';
+
+ /**
+ * Safe: Yes
+ * Idempotent: Yes
+ * Cacheable: Yes
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
+ */
+ public const HEAD = 'HEAD';
+
+ /**
+ * Safe: Yes
+ * Idempotent: Yes
+ * Cacheable: No
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
+ */
+ public const OPTIONS = 'OPTIONS';
+
+ /**
+ * Safe: No
+ * Idempotent: No
+ * Cacheable: Only if freshness information is included
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH
+ */
+ public const PATCH = 'PATCH';
+
+ /**
+ * Safe: No
+ * Idempotent: No
+ * Cacheable: Only if freshness information is included
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
+ */
+ public const POST = 'POST';
+
+ /**
+ * Safe: No
+ * Idempotent: Yes
+ * Cacheable: No
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
+ */
+ public const PUT = 'PUT';
+
+ /**
+ * Safe: Yes
+ * Idempotent: Yes
+ * Cacheable: No
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE
+ */
+ public const TRACE = 'TRACE';
+
+ /**
+ * Returns all HTTP methods.
+ *
+ * @return list
+ */
+ public static function all(): array
+ {
+ return [
+ self::CONNECT,
+ self::DELETE,
+ self::GET,
+ self::HEAD,
+ self::OPTIONS,
+ self::PATCH,
+ self::POST,
+ self::PUT,
+ self::TRACE,
+ ];
+ }
+}
diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php
index 61c3a58f4af7..67a03a38014d 100644
--- a/system/HTTP/Negotiate.php
+++ b/system/HTTP/Negotiate.php
@@ -1,5 +1,7 @@
method) : strtolower($this->method);
+ return $this->method;
}
/**
diff --git a/system/HTTP/OutgoingRequestInterface.php b/system/HTTP/OutgoingRequestInterface.php
index 3839b64fd8e1..da9e36206c37 100644
--- a/system/HTTP/OutgoingRequestInterface.php
+++ b/system/HTTP/OutgoingRequestInterface.php
@@ -1,5 +1,7 @@
reverseRoute($route, ...$params);
+ $route = service('routes')->reverseRoute($route, ...$params);
if (! $route) {
throw HTTPException::forInvalidRedirectRoute($namedRoute);
@@ -75,7 +77,7 @@ public function route(string $route, array $params = [], ?int $code = null, stri
*/
public function back(?int $code = null, string $method = 'auto')
{
- Services::session();
+ service('session');
return $this->redirect(previous_url(), $method, $code);
}
@@ -90,7 +92,7 @@ public function back(?int $code = null, string $method = 'auto')
*/
public function withInput()
{
- $session = Services::session();
+ $session = service('session');
$session->setFlashdata('_ci_old_input', [
'get' => $_GET ?? [],
'post' => $_POST ?? [],
@@ -112,10 +114,10 @@ public function withInput()
*/
private function withErrors(): self
{
- $validation = Services::validation();
+ $validation = service('validation');
if ($validation->getErrors()) {
- $session = Services::session();
+ $session = service('session');
$session->setFlashdata('_ci_validation_errors', $validation->getErrors());
}
@@ -131,7 +133,7 @@ private function withErrors(): self
*/
public function with(string $key, $message)
{
- Services::session()->setFlashdata($key, $message);
+ service('session')->setFlashdata($key, $message);
return $this;
}
@@ -161,8 +163,14 @@ public function withCookies()
*/
public function withHeaders()
{
- foreach (Services::response()->headers() as $name => $header) {
- $this->setHeader($name, $header->getValue());
+ foreach (Services::response()->headers() as $name => $value) {
+ if ($value instanceof Header) {
+ $this->setHeader($name, $value->getValue());
+ } else {
+ foreach ($value as $header) {
+ $this->addHeader($name, $header->getValue());
+ }
+ }
}
return $this;
diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php
index afb0b22ad5d0..125646b6c7c7 100644
--- a/system/HTTP/Request.php
+++ b/system/HTTP/Request.php
@@ -1,5 +1,7 @@
- *
- * @deprecated 4.0.5 No longer used. Check the App config directly
- */
- protected $proxyIPs;
-
/**
* Constructor.
*
* @param App $config
- *
- * @deprecated 4.0.5 The $config is no longer needed and will be removed in a future version
*/
- public function __construct($config = null) // @phpstan-ignore-line
+ public function __construct($config = null)
{
+ $this->config = $config ?? config(App::class);
+
if (empty($this->method)) {
- $this->method = $this->getServer('REQUEST_METHOD') ?? 'GET';
+ $this->method = $this->getServer('REQUEST_METHOD') ?? Method::GET;
}
if (empty($this->uri)) {
@@ -50,35 +42,6 @@ public function __construct($config = null) // @phpstan-ignore-line
}
}
- /**
- * Validate an IP address
- *
- * @param string $ip IP Address
- * @param string $which IP protocol: 'ipv4' or 'ipv6'
- *
- * @deprecated 4.0.5 Use Validation instead
- *
- * @codeCoverageIgnore
- */
- public function isValidIP(?string $ip = null, ?string $which = null): bool
- {
- return (new FormatRules())->valid_ip($ip, $which);
- }
-
- /**
- * Get the request method.
- *
- * @param bool $upper Whether to return in upper or lower case.
- *
- * @deprecated 4.0.5 The $upper functionality will be removed and this will revert to its PSR-7 equivalent
- *
- * @codeCoverageIgnore
- */
- public function getMethod(bool $upper = false): string
- {
- return ($upper) ? strtoupper($this->method) : strtolower($this->method);
- }
-
/**
* Sets the request method. Used when spoofing the request.
*
diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php
index 367c494928b8..26af51df9dab 100644
--- a/system/HTTP/RequestInterface.php
+++ b/system/HTTP/RequestInterface.php
@@ -1,5 +1,7 @@
proxyIPs;
+ $proxyIPs = $this->config->proxyIPs;
if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) {
throw new ConfigException(
@@ -77,7 +86,7 @@ public function getIPAddress(): string
// @TODO Extract all this IP address logic to another class.
foreach ($proxyIPs as $proxyIP => $header) {
// Check if we have an IP address or a subnet
- if (strpos($proxyIP, '/') === false) {
+ if (! str_contains($proxyIP, '/')) {
// An IP address (and not a subnet) is specified.
// We can compare right away.
if ($proxyIP === $this->ipAddress) {
@@ -98,7 +107,7 @@ public function getIPAddress(): string
}
// If the proxy entry doesn't match the IP protocol - skip it
- if (strpos($proxyIP, $separator) === false) {
+ if (! str_contains($proxyIP, $separator)) {
continue;
}
diff --git a/system/HTTP/ResponsableInterface.php b/system/HTTP/ResponsableInterface.php
index 0cca5356f1f7..41bff03909e7 100644
--- a/system/HTTP/ResponsableInterface.php
+++ b/system/HTTP/ResponsableInterface.php
@@ -1,5 +1,7 @@
CSP = Services::csp();
- $this->CSPEnabled = $config->CSPEnabled;
-
$this->cookieStore = new CookieStore([]);
$cookie = config(CookieConfig::class);
diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php
index 4455bc648ee0..827eea44e298 100644
--- a/system/HTTP/ResponseInterface.php
+++ b/system/HTTP/ResponseInterface.php
@@ -1,5 +1,7 @@
CSP->enabled() instead.
- */
- protected $CSPEnabled = false;
-
/**
* Content security policy handler
*
* @var ContentSecurityPolicy
- *
- * @deprecated Will be protected. Use `getCSP()` instead.
*/
- public $CSP;
+ protected $CSP;
/**
* CookieStore instance.
@@ -59,69 +49,6 @@ trait ResponseTrait
*/
protected $cookieStore;
- /**
- * Set a cookie name prefix if you need to avoid collisions
- *
- * @var string
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookiePrefix = '';
-
- /**
- * Set to .your-domain.com for site-wide cookies
- *
- * @var string
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookieDomain = '';
-
- /**
- * Typically will be a forward slash
- *
- * @var string
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookiePath = '/';
-
- /**
- * Cookie will only be set if a secure HTTPS connection exists.
- *
- * @var bool
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookieSecure = false;
-
- /**
- * Cookie will only be accessible via HTTP(S) (no javascript)
- *
- * @var bool
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookieHTTPOnly = false;
-
- /**
- * Cookie SameSite setting
- *
- * @var string
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookieSameSite = Cookie::SAMESITE_LAX;
-
- /**
- * Stores all cookies that were set in the response.
- *
- * @var array
- *
- * @deprecated Use the dedicated Cookie class instead.
- */
- protected $cookies = [];
-
/**
* Type of format the body is in.
* Valid: html, json, xml
@@ -262,7 +189,7 @@ public function getJSON()
$body = $this->body;
if ($this->bodyFormat !== 'json') {
- $body = Services::format()->getFormatter('application/json')->format($body);
+ $body = service('format')->getFormatter('application/json')->format($body);
}
return $body ?: null;
@@ -294,7 +221,7 @@ public function getXML()
$body = $this->body;
if ($this->bodyFormat !== 'xml') {
- $body = Services::format()->getFormatter('application/xml')->format($body);
+ $body = service('format')->getFormatter('application/xml')->format($body);
}
return $body;
@@ -319,7 +246,7 @@ protected function formatBody($body, string $format)
// Nothing much to do for a string...
if (! is_string($body) || $format === 'json-unencoded') {
- $body = Services::format()->getFormatter($mime)->format($body);
+ $body = service('format')->getFormatter($mime)->format($body);
}
return $body;
@@ -472,8 +399,22 @@ public function sendHeaders()
header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode());
// Send all of our headers
- foreach (array_keys($this->headers()) as $name) {
- header($name . ': ' . $this->getHeaderLine($name), false, $this->getStatusCode());
+ foreach ($this->headers() as $name => $value) {
+ if ($value instanceof Header) {
+ header(
+ $name . ': ' . $value->getValueLine(),
+ false,
+ $this->getStatusCode()
+ );
+ } else {
+ foreach ($value as $header) {
+ header(
+ $name . ': ' . $header->getValueLine(),
+ false,
+ $this->getStatusCode()
+ );
+ }
+ }
}
return $this;
@@ -507,7 +448,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
if (
$method === 'auto'
&& isset($_SERVER['SERVER_SOFTWARE'])
- && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
+ && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')
) {
$method = 'refresh';
} elseif ($method !== 'refresh' && $code === null) {
@@ -516,9 +457,9 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD'])
&& $this->getProtocolVersion() >= 1.1
) {
- if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+ if ($_SERVER['REQUEST_METHOD'] === Method::GET) {
$code = 302;
- } elseif (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'DELETE'], true)) {
+ } elseif (in_array($_SERVER['REQUEST_METHOD'], [Method::POST, Method::PUT, Method::DELETE], true)) {
// reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
$code = 303;
} else {
@@ -531,15 +472,10 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
$code = 302;
}
- switch ($method) {
- case 'refresh':
- $this->setHeader('Refresh', '0;url=' . $uri);
- break;
-
- default:
- $this->setHeader('Location', $uri);
- break;
- }
+ match ($method) {
+ 'refresh' => $this->setHeader('Refresh', '0;url=' . $uri),
+ default => $this->setHeader('Location', $uri),
+ };
$this->setStatusCode($code);
@@ -554,7 +490,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
*
* @param array|Cookie|string $name Cookie name / array containing binds / Cookie object
* @param string $value Cookie value
- * @param string $expire Cookie expiration time in seconds
+ * @param int $expire Cookie expiration time in seconds
* @param string $domain Cookie domain (e.g.: '.yourdomain.com')
* @param string $path Cookie path (default: '/')
* @param string $prefix Cookie name prefix ('': the default prefix)
@@ -567,7 +503,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
public function setCookie(
$name,
$value = '',
- $expire = '',
+ $expire = 0,
$domain = '',
$path = '/',
$prefix = '',
@@ -581,14 +517,11 @@ public function setCookie(
return $this;
}
- /** @var CookieConfig|null $cookieConfig */
$cookieConfig = config(CookieConfig::class);
- if ($cookieConfig instanceof CookieConfig) {
- $secure ??= $cookieConfig->secure;
- $httponly ??= $cookieConfig->httponly;
- $samesite ??= $cookieConfig->samesite;
- }
+ $secure ??= $cookieConfig->secure;
+ $httponly ??= $cookieConfig->httponly;
+ $samesite ??= $cookieConfig->samesite;
if (is_array($name)) {
// always leave 'name' in last place, as the loop will break otherwise, due to ${$item}
@@ -700,7 +633,7 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat
}
if (! $found) {
- $this->setCookie($name, '', '', $domain, $path, $prefix);
+ $this->setCookie($name, '', 0, $domain, $path, $prefix);
}
return $this;
@@ -733,7 +666,7 @@ protected function sendCookies()
private function dispatchCookies(): void
{
/** @var IncomingRequest $request */
- $request = Services::request();
+ $request = service('request');
foreach ($this->cookieStore->display() as $cookie) {
if ($cookie->isSecure() && ! $request->isSecure()) {
diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php
index 8daa5ab2c0cd..def61b65e17b 100644
--- a/system/HTTP/SiteURI.php
+++ b/system/HTTP/SiteURI.php
@@ -1,5 +1,7 @@
appConfig = $appConfig;
- $this->superglobals = $superglobals;
}
/**
@@ -95,20 +92,11 @@ public function detectRoutePath(string $protocol = ''): string
$protocol = $this->appConfig->uriProtocol;
}
- switch ($protocol) {
- case 'REQUEST_URI':
- $routePath = $this->parseRequestURI();
- break;
-
- case 'QUERY_STRING':
- $routePath = $this->parseQueryString();
- break;
-
- case 'PATH_INFO':
- default:
- $routePath = $this->superglobals->server($protocol) ?? $this->parseRequestURI();
- break;
- }
+ $routePath = match ($protocol) {
+ 'REQUEST_URI' => $this->parseRequestURI(),
+ 'QUERY_STRING' => $this->parseQueryString(),
+ default => $this->superglobals->server($protocol) ?? $this->parseRequestURI(),
+ };
return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/');
}
@@ -161,7 +149,7 @@ private function parseRequestURI(): string
// This section ensures that even on servers that require the URI to
// contain the query string (Nginx) a correct URI is found, and also
// fixes the QUERY_STRING Server var and $_GET array.
- if (trim($path, '/') === '' && strncmp($query, '/', 1) === 0) {
+ if (trim($path, '/') === '' && str_starts_with($query, '/')) {
$parts = explode('?', $query, 2);
$path = $parts[0];
$newQuery = $query[1] ?? '';
@@ -193,7 +181,7 @@ private function parseQueryString(): string
return '/';
}
- if (strncmp($query, '/', 1) === 0) {
+ if (str_starts_with($query, '/')) {
$parts = explode('?', $query, 2);
$path = $parts[0];
$newQuery = $parts[1] ?? '';
diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php
index 5bec38013bf2..efa99cf778a8 100644
--- a/system/HTTP/URI.php
+++ b/system/HTTP/URI.php
@@ -1,5 +1,7 @@
baseURL);
if (
- substr($this->getScheme(), 0, 4) === 'http'
+ str_starts_with($this->getScheme(), 'http')
&& $this->getHost() === $baseUri->getHost()
) {
// Check for additional segments
$basePath = trim($baseUri->getPath(), '/') . '/';
$trimPath = ltrim($path, '/');
- if ($basePath !== '/' && strpos($trimPath, $basePath) !== 0) {
+ if ($basePath !== '/' && ! str_starts_with($trimPath, $basePath)) {
$path = $basePath . $trimPath;
}
@@ -877,7 +880,7 @@ public function refreshPath()
*/
public function setQuery(string $query)
{
- if (strpos($query, '#') !== false) {
+ if (str_contains($query, '#')) {
if ($this->silent) {
return $this;
}
@@ -886,7 +889,7 @@ public function setQuery(string $query)
}
// Can't have leading ?
- if ($query !== '' && strpos($query, '?') === 0) {
+ if ($query !== '' && str_starts_with($query, '?')) {
$query = substr($query, 1);
}
@@ -1008,10 +1011,10 @@ protected function filterPath(?string $path = null): string
$path = self::removeDotSegments($path);
// Fix up some leading slash edge cases...
- if (strpos($orig, './') === 0) {
+ if (str_starts_with($orig, './')) {
$path = '/' . $path;
}
- if (strpos($orig, '../') === 0) {
+ if (str_starts_with($orig, '../')) {
$path = '/' . $path;
}
@@ -1112,7 +1115,7 @@ public function resolveRelativeURI(string $uri)
$transformed->setQuery($this->getQuery());
}
} else {
- if (strpos($relative->getPath(), '/') === 0) {
+ if (str_starts_with($relative->getPath(), '/')) {
$transformed->setPath($relative->getPath());
} else {
$transformed->setPath($this->mergePaths($this, $relative));
diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php
index 2294b02a2b48..6a5104779a92 100644
--- a/system/HTTP/UserAgent.php
+++ b/system/HTTP/UserAgent.php
@@ -1,5 +1,7 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Helpers\Array;
+
+use InvalidArgumentException;
+
+/**
+ * @interal This is internal implementation for the framework.
+ *
+ * If there are any methods that should be provided, make them
+ * public APIs via helper functions.
+ *
+ * @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest
+ * @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest
+ * @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest
+ */
+final class ArrayHelper
+{
+ /**
+ * Searches an array through dot syntax. Supports wildcard searches,
+ * like `foo.*.bar`.
+ *
+ * @used-by dot_array_search()
+ *
+ * @param string $index The index as dot array syntax.
+ *
+ * @return array|bool|int|object|string|null
+ */
+ public static function dotSearch(string $index, array $array)
+ {
+ return self::arraySearchDot(self::convertToArray($index), $array);
+ }
+
+ /**
+ * @param string $index The index as dot array syntax.
+ *
+ * @return list The index as an array.
+ */
+ private static function convertToArray(string $index): array
+ {
+ // See https://regex101.com/r/44Ipql/1
+ $segments = preg_split(
+ '/(? str_replace('\.', '.', $key),
+ $segments
+ );
+ }
+
+ /**
+ * Recursively search the array with wildcards.
+ *
+ * @used-by dotSearch()
+ *
+ * @return array|bool|float|int|object|string|null
+ */
+ private static function arraySearchDot(array $indexes, array $array)
+ {
+ // If index is empty, returns null.
+ if ($indexes === []) {
+ return null;
+ }
+
+ // Grab the current index
+ $currentIndex = array_shift($indexes);
+
+ if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
+ return null;
+ }
+
+ // Handle Wildcard (*)
+ if ($currentIndex === '*') {
+ $answer = [];
+
+ foreach ($array as $value) {
+ if (! is_array($value)) {
+ return null;
+ }
+
+ $answer[] = self::arraySearchDot($indexes, $value);
+ }
+
+ $answer = array_filter($answer, static fn ($value) => $value !== null);
+
+ if ($answer !== []) {
+ // If array only has one element, we return that element for BC.
+ return count($answer) === 1 ? current($answer) : $answer;
+ }
+
+ return null;
+ }
+
+ // If this is the last index, make sure to return it now,
+ // and not try to recurse through things.
+ if ($indexes === []) {
+ return $array[$currentIndex];
+ }
+
+ // Do we need to recursively search this value?
+ if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
+ return self::arraySearchDot($indexes, $array[$currentIndex]);
+ }
+
+ // Otherwise, not found.
+ return null;
+ }
+
+ /**
+ * array_key_exists() with dot array syntax.
+ *
+ * If wildcard `*` is used, all items for the key after it must have the key.
+ */
+ public static function dotKeyExists(string $index, array $array): bool
+ {
+ if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
+ throw new InvalidArgumentException(
+ 'You must set key right after "*". Invalid index: "' . $index . '"'
+ );
+ }
+
+ $indexes = self::convertToArray($index);
+
+ // If indexes is empty, returns false.
+ if ($indexes === []) {
+ return false;
+ }
+
+ $currentArray = $array;
+
+ // Grab the current index
+ while ($currentIndex = array_shift($indexes)) {
+ if ($currentIndex === '*') {
+ $currentIndex = array_shift($indexes);
+
+ foreach ($currentArray as $item) {
+ if (! array_key_exists($currentIndex, $item)) {
+ return false;
+ }
+ }
+
+ // If indexes is empty, all elements are checked.
+ if ($indexes === []) {
+ return true;
+ }
+
+ $currentArray = self::dotSearch('*.' . $currentIndex, $currentArray);
+
+ continue;
+ }
+
+ if (! array_key_exists($currentIndex, $currentArray)) {
+ return false;
+ }
+
+ $currentArray = $currentArray[$currentIndex];
+ }
+
+ return true;
+ }
+
+ /**
+ * Groups all rows by their index values. Result's depth equals number of indexes
+ *
+ * @used-by array_group_by()
+ *
+ * @param array $array Data array (i.e. from query result)
+ * @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty
+ * @param bool $includeEmpty If true, null and '' are also added as valid keys to group
+ *
+ * @return array Result array where rows are grouped together by indexes values.
+ */
+ public static function groupBy(array $array, array $indexes, bool $includeEmpty = false): array
+ {
+ if ($indexes === []) {
+ return $array;
+ }
+
+ $result = [];
+
+ foreach ($array as $row) {
+ $result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Recursively attach $row to the $indexes path of values found by
+ * `dot_array_search()`.
+ *
+ * @used-by groupBy()
+ */
+ private static function arrayAttachIndexedValue(
+ array $result,
+ array $row,
+ array $indexes,
+ bool $includeEmpty
+ ): array {
+ if (($index = array_shift($indexes)) === null) {
+ $result[] = $row;
+
+ return $result;
+ }
+
+ $value = dot_array_search($index, $row);
+
+ if (! is_scalar($value)) {
+ $value = '';
+ }
+
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+
+ if (! $includeEmpty && $value === '') {
+ return $result;
+ }
+
+ if (! array_key_exists($value, $result)) {
+ $result[$value] = [];
+ }
+
+ $result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty);
+
+ return $result;
+ }
+
+ /**
+ * Compare recursively two associative arrays and return difference as new array.
+ * Returns keys that exist in `$original` but not in `$compareWith`.
+ */
+ public static function recursiveDiff(array $original, array $compareWith): array
+ {
+ $difference = [];
+
+ if ($original === []) {
+ return [];
+ }
+
+ if ($compareWith === []) {
+ return $original;
+ }
+
+ foreach ($original as $originalKey => $originalValue) {
+ if ($originalValue === []) {
+ continue;
+ }
+
+ if (is_array($originalValue)) {
+ $diffArrays = [];
+
+ if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
+ $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
+ } else {
+ $difference[$originalKey] = $originalValue;
+ }
+
+ if ($diffArrays !== []) {
+ $difference[$originalKey] = $diffArrays;
+ }
+ } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
+ $difference[$originalKey] = $originalValue;
+ }
+ }
+
+ return $difference;
+ }
+
+ /**
+ * Recursively count all keys.
+ */
+ public static function recursiveCount(array $array, int $counter = 0): int
+ {
+ foreach ($array as $value) {
+ if (is_array($value)) {
+ $counter = self::recursiveCount($value, $counter);
+ }
+
+ $counter++;
+ }
+
+ return $counter;
+ }
+
+ /**
+ * Sorts array values in natural order
+ * If the value is an array, you need to specify the $sortByIndex of the key to sort
+ *
+ * @param list|string> $array
+ * @param int|string|null $sortByIndex
+ */
+ public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool
+ {
+ return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex) {
+ if ($sortByIndex !== null) {
+ return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]);
+ }
+
+ return strnatcmp((string) $currentValue, (string) $nextValue);
+ });
+ }
+}
diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php
index 4c1477f57d4a..837a612e4cef 100644
--- a/system/Helpers/array_helper.php
+++ b/system/Helpers/array_helper.php
@@ -1,5 +1,7 @@
str_replace('\.', '.', $key), $segments);
-
- return _array_search_dot($segments, $array);
- }
-}
-
-if (! function_exists('_array_search_dot')) {
- /**
- * Used by `dot_array_search` to recursively search the
- * array with wildcards.
- *
- * @internal This should not be used on its own.
- *
- * @return array|bool|float|int|object|string|null
- */
- function _array_search_dot(array $indexes, array $array)
- {
- // If index is empty, returns null.
- if ($indexes === []) {
- return null;
- }
-
- // Grab the current index
- $currentIndex = array_shift($indexes);
-
- if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
- return null;
- }
-
- // Handle Wildcard (*)
- if ($currentIndex === '*') {
- $answer = [];
-
- foreach ($array as $value) {
- if (! is_array($value)) {
- return null;
- }
-
- $answer[] = _array_search_dot($indexes, $value);
- }
-
- $answer = array_filter($answer, static fn ($value) => $value !== null);
-
- if ($answer !== []) {
- // If array only has one element, we return that element for BC.
- return count($answer) === 1 ? current($answer) : $answer;
- }
-
- return null;
- }
-
- // If this is the last index, make sure to return it now,
- // and not try to recurse through things.
- if ($indexes === []) {
- return $array[$currentIndex];
- }
-
- // Do we need to recursively search this value?
- if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
- return _array_search_dot($indexes, $array[$currentIndex]);
- }
-
- // Otherwise, not found.
- return null;
+ return ArrayHelper::dotSearch($index, $array);
}
}
@@ -227,55 +160,6 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array
*/
function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array
{
- if ($indexes === []) {
- return $array;
- }
-
- $result = [];
-
- foreach ($array as $row) {
- $result = _array_attach_indexed_value($result, $row, $indexes, $includeEmpty);
- }
-
- return $result;
- }
-}
-
-if (! function_exists('_array_attach_indexed_value')) {
- /**
- * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by
- * `dot_array_search`
- *
- * @internal This should not be used on its own
- */
- function _array_attach_indexed_value(array $result, array $row, array $indexes, bool $includeEmpty): array
- {
- if (($index = array_shift($indexes)) === null) {
- $result[] = $row;
-
- return $result;
- }
-
- $value = dot_array_search($index, $row);
-
- if (! is_scalar($value)) {
- $value = '';
- }
-
- if (is_bool($value)) {
- $value = (int) $value;
- }
-
- if (! $includeEmpty && $value === '') {
- return $result;
- }
-
- if (! array_key_exists($value, $result)) {
- $result[$value] = [];
- }
-
- $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty);
-
- return $result;
+ return ArrayHelper::groupBy($array, $indexes, $includeEmpty);
}
}
diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php
index de8fb6146a63..49e568346b68 100755
--- a/system/Helpers/cookie_helper.php
+++ b/system/Helpers/cookie_helper.php
@@ -1,5 +1,7 @@
setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly, $sameSite);
}
}
@@ -75,7 +76,7 @@ function get_cookie($index, bool $xssClean = false, ?string $prefix = '')
$prefix = $cookie->prefix;
}
- $request = Services::request();
+ $request = service('request');
$filter = $xssClean ? FILTER_SANITIZE_FULL_SPECIAL_CHARS : FILTER_DEFAULT;
return $request->getCookie($prefix . $index, $filter);
@@ -97,7 +98,7 @@ function get_cookie($index, bool $xssClean = false, ?string $prefix = '')
*/
function delete_cookie($name, string $domain = '', string $path = '/', string $prefix = '')
{
- Services::response()->deleteCookie($name, $domain, $path, $prefix);
+ service('response')->deleteCookie($name, $domain, $path, $prefix);
}
}
@@ -107,6 +108,6 @@ function delete_cookie($name, string $domain = '', string $path = '/', string $p
*/
function has_cookie(string $name, ?string $value = null, string $prefix = ''): bool
{
- return Services::response()->hasCookie($name, $value, $prefix);
+ return service('response')->hasCookie($name, $value, $prefix);
}
}
diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php
index cf1dc33a74ea..c0b4d5ebe17a 100644
--- a/system/Helpers/date_helper.php
+++ b/system/Helpers/date_helper.php
@@ -1,5 +1,7 @@
getLocale(), $action);
+ if (str_contains($action, '{locale}')) {
+ $action = str_replace('{locale}', service('request')->getLocale(), $action);
}
$action = site_url($action);
@@ -59,9 +60,9 @@ function form_open(string $action = '', $attributes = [], array $hidden = []): s
$form = '