From 636b38cbf644bfc295d578672e8b1f9df101fd31 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 29 Jan 2023 21:20:47 +0900 Subject: [PATCH 01/26] test: remove unneeded code --- .../HTTP/IncomingRequestDetectingTest.php | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index ab0bfb5c6092..88b69fe6b028 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -31,127 +31,139 @@ protected function setUp(): void $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; - $origin = 'http://www.example.com/index.php/woot?code=good#pos'; - + // The URI object is not used in detectPath(). + $origin = 'http://www.example.com/index.php/woot?code=good#pos'; $this->request = new IncomingRequest(new App(), new URI($origin), null, new UserAgent()); } public function testPathDefault() { - $this->request->uri = '/index.php/woot?code=good#pos'; + // /index.php/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/index.php/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath()); } public function testPathEmpty() { - $this->request->uri = '/'; + // / $_SERVER['REQUEST_URI'] = '/'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = '/'; + + $expected = '/'; $this->assertSame($expected, $this->request->detectPath()); } public function testPathRequestURI() { - $this->request->uri = '/index.php/woot?code=good#pos'; + // /index.php/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/index.php/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURINested() { - $this->request->uri = '/ci/index.php/woot?code=good#pos'; + // /ci/index.php/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/index.php/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURISubfolder() { - $this->request->uri = '/ci/index.php/popcorn/woot?code=good#pos'; + // /ci/index.php/popcorn/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/ci/index.php/popcorn/woot'; $_SERVER['SCRIPT_NAME'] = '/ci/index.php'; - $expected = 'popcorn/woot'; + + $expected = 'popcorn/woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURINoIndex() { - $this->request->uri = '/sub/example'; + // /sub/example $_SERVER['REQUEST_URI'] = '/sub/example'; $_SERVER['SCRIPT_NAME'] = '/sub/index.php'; - $expected = 'example'; + + $expected = 'example'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURINginx() { - $this->request->uri = '/ci/index.php/woot?code=good#pos'; + // /ci/index.php/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURINginxRedirecting() { - $this->request->uri = '/?/ci/index.php/woot'; + // /?/ci/index.php/woot $_SERVER['REQUEST_URI'] = '/?/ci/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'ci/woot'; + + $expected = 'ci/woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathRequestURISuppressed() { - $this->request->uri = '/woot?code=good#pos'; + // /woot?code=good#pos $_SERVER['REQUEST_URI'] = '/woot'; $_SERVER['SCRIPT_NAME'] = '/'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath('REQUEST_URI')); } public function testPathQueryString() { - $this->request->uri = '/?/ci/index.php/woot'; + // /?/ci/index.php/woot $_SERVER['REQUEST_URI'] = '/?/ci/woot'; $_SERVER['QUERY_STRING'] = '/ci/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'ci/woot'; + + $expected = 'ci/woot'; $this->assertSame($expected, $this->request->detectPath('QUERY_STRING')); } public function testPathQueryStringEmpty() { - $this->request->uri = '/?/ci/index.php/woot'; + // /?/ci/index.php/woot $_SERVER['REQUEST_URI'] = '/?/ci/woot'; $_SERVER['QUERY_STRING'] = ''; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = ''; + + $expected = ''; $this->assertSame($expected, $this->request->detectPath('QUERY_STRING')); } public function testPathPathInfo() { - $this->request->uri = '/index.php/woot?code=good#pos'; + // /index.php/woot?code=good#pos $this->request->setGlobal('server', [ 'PATH_INFO' => null, ]); $_SERVER['REQUEST_URI'] = '/index.php/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = 'woot'; + + $expected = 'woot'; $this->assertSame($expected, $this->request->detectPath('PATH_INFO')); } public function testPathPathInfoGlobal() { - $this->request->uri = '/index.php/woot?code=good#pos'; + // /index.php/woot?code=good#pos $this->request->setGlobal('server', [ 'PATH_INFO' => 'silliness', ]); From 7503d70386d0267f020d0b9a6a90c36282754fa6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 29 Jan 2023 21:47:26 +0900 Subject: [PATCH 02/26] test: fix incorrect tests --- tests/system/HTTP/IncomingRequestDetectingTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index 88b69fe6b028..b326e238f59b 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -128,8 +128,8 @@ public function testPathRequestURISuppressed() public function testPathQueryString() { - // /?/ci/index.php/woot - $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + // /index.php?/ci/woot + $_SERVER['REQUEST_URI'] = '/index.php?/ci/woot'; $_SERVER['QUERY_STRING'] = '/ci/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php'; @@ -139,8 +139,8 @@ public function testPathQueryString() public function testPathQueryStringEmpty() { - // /?/ci/index.php/woot - $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + // /index.php? + $_SERVER['REQUEST_URI'] = '/index.php?'; $_SERVER['QUERY_STRING'] = ''; $_SERVER['SCRIPT_NAME'] = '/index.php'; From 3eab713e5c30c7e984e1a5110184119a79857b45 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 29 Jan 2023 21:47:39 +0900 Subject: [PATCH 03/26] test: add test --- tests/system/HTTP/IncomingRequestDetectingTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index b326e238f59b..81bdfcdc5306 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -137,6 +137,17 @@ public function testPathQueryString() $this->assertSame($expected, $this->request->detectPath('QUERY_STRING')); } + public function testPathQueryStringWithQueryString() + { + // /index.php?/ci/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php?/ci/woot?code=good'; + $_SERVER['QUERY_STRING'] = '/ci/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $expected = 'ci/woot'; + $this->assertSame($expected, $this->request->detectPath('QUERY_STRING')); + } + public function testPathQueryStringEmpty() { // /index.php? From 97002d0b114d41669fddf8598b9fb6c0e497497a Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:27:50 +0900 Subject: [PATCH 04/26] docs: add PHPDocs --- system/Router/Router.php | 4 ++++ system/Router/RouterInterface.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/system/Router/Router.php b/system/Router/Router.php index 137aa4e43af9..b0247b2d93f4 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -155,6 +155,10 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request } /** + * Finds the controller method corresponding to the URI. + * + * @param string|null $uri URI path relative to baseURL + * * @return Closure|string Controller classname or Closure * * @throws PageNotFoundException diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index 1efeb2e2c6e2..ffed59ca8aac 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -27,7 +27,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request /** * Finds the controller method corresponding to the URI. * - * @param string $uri + * @param string|null $uri URI path relative to baseURL * * @return Closure|string Controller classname or Closure */ From e497c49e1bbc4c2789f432daa33e484a0824bf62 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:50:15 +0900 Subject: [PATCH 05/26] fix: if uri is empty string, then it should be `/` Make it the same behavior as when URIProtocol is REQUEST_URI. --- system/HTTP/IncomingRequest.php | 2 +- tests/system/HTTP/IncomingRequestDetectingTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 3304f4664a76..a3a8de837759 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -329,7 +329,7 @@ protected function parseQueryString(): string $uri = $_SERVER['QUERY_STRING'] ?? @getenv('QUERY_STRING'); if (trim($uri, '/') === '') { - return ''; + return '/'; } if (strncmp($uri, '/', 1) === 0) { diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index 81bdfcdc5306..8f265ab13a84 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -155,7 +155,7 @@ public function testPathQueryStringEmpty() $_SERVER['QUERY_STRING'] = ''; $_SERVER['SCRIPT_NAME'] = '/index.php'; - $expected = ''; + $expected = '/'; $this->assertSame($expected, $this->request->detectPath('QUERY_STRING')); } From edef908890c5ed54ace942133248fb86d04bbe6a Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:53:39 +0900 Subject: [PATCH 06/26] fix: defined `/` route may be ignored --- system/Router/Router.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/system/Router/Router.php b/system/Router/Router.php index b0247b2d93f4..8dd35c656e39 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -166,12 +166,9 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request */ public function handle(?string $uri = null) { - // If we cannot find a URI to match against, then - // everything runs off of its default settings. + // If we cannot find a URI to match against, then set it to root (`/`). if ($uri === null || $uri === '') { - return strpos($this->controller, '\\') === false - ? $this->collection->getDefaultNamespace() . $this->controller - : $this->controller; + $uri = '/'; } // Decode URL-encoded string From 021c5bbde49b4b04ea04415e15c0400421e063ed Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:55:26 +0900 Subject: [PATCH 07/26] test: improve test method name --- tests/system/HTTP/IncomingRequestDetectingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index 8f265ab13a84..f3be0aeb2247 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -46,7 +46,7 @@ public function testPathDefault() $this->assertSame($expected, $this->request->detectPath()); } - public function testPathEmpty() + public function testPathDefaultEmpty() { // / $_SERVER['REQUEST_URI'] = '/'; From aa4a360bfa706e7864572e59ae20fc924255d1b3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:55:49 +0900 Subject: [PATCH 08/26] test: update test The auto routing is disabled, so defined route should be used, or 404 should happen. --- tests/system/Router/RouterTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 5e7733ee4ff4..5be01a47bfc6 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -39,6 +39,7 @@ protected function setUp(): void $this->collection = new RouteCollection(Services::locator(), $moduleConfig); $routes = [ + '/' => 'Home::index', 'users' => 'Users::index', 'user-setting/show-list' => 'User_setting::show_list', 'user-setting/(:segment)' => 'User_setting::detail/$1', @@ -56,20 +57,20 @@ protected function setUp(): void 'objects/(:segment)/sort/(:segment)/([A-Z]{3,7})' => 'AdminList::objectsSortCreate/$1/$2/$3', '(:segment)/(:segment)/(:segment)' => '$2::$3/$1', ]; - $this->collection->map($routes); + $this->request = Services::request(); $this->request->setMethod('get'); } - public function testEmptyURIMatchesDefaults() + public function testEmptyURIMatchesRoot() { $router = new Router($this->collection, $this->request); $router->handle(''); - $this->assertSame($this->collection->getDefaultController(), $router->controllerName()); - $this->assertSame($this->collection->getDefaultMethod(), $router->methodName()); + $this->assertSame('\Home', $router->controllerName()); + $this->assertSame('index', $router->methodName()); } public function testZeroAsURIPath() From 0b3460baaa9718d5e9d14dd7673df58f812bae9d Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Jan 2023 13:58:26 +0900 Subject: [PATCH 09/26] chore: update phpstan-baseline.neon.dist --- phpstan-baseline.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index 4259df0cc3ef..2d1f9ddd3c41 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -217,7 +217,7 @@ parameters: - message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getDefaultNamespace\\(\\)\\.$#" - count: 3 + count: 2 path: system/Router/Router.php - From 64cd95cdd17495f3f643f25315a2c77e7579e611 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Feb 2023 13:24:25 +0900 Subject: [PATCH 10/26] fix: the stack trace displayed when Exception handler runs out of memory is useless --- system/Debug/Exceptions.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index bcce589db894..6dc098d4af10 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -68,6 +68,8 @@ class Exceptions */ protected $response; + private ?Throwable $exceptionCaughtByExceptionHandler = null; + /** * @param CLIRequest|IncomingRequest $request */ @@ -113,6 +115,8 @@ public function initialize() */ public function exceptionHandler(Throwable $exception) { + $this->exceptionCaughtByExceptionHandler = $exception; + [$statusCode, $exitCode] = $this->determineCodes($exception); if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { @@ -191,6 +195,13 @@ public function shutdownHandler() ['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error; + if ($this->exceptionCaughtByExceptionHandler) { + $message .= "\n【Previous Exception】\n" + . get_class($this->exceptionCaughtByExceptionHandler) . "\n" + . $this->exceptionCaughtByExceptionHandler->getMessage() . "\n" + . $this->exceptionCaughtByExceptionHandler->getTraceAsString(); + } + if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) { $this->exceptionHandler(new ErrorException($message, 0, $type, $file, $line)); } From cf670a2bec208ede6238e8a72ab3a5616aa7421d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 3 Feb 2023 18:43:54 +0900 Subject: [PATCH 11/26] chore: update Kint to 5.0.3 --- system/ThirdParty/Kint/Utils.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/ThirdParty/Kint/Utils.php b/system/ThirdParty/Kint/Utils.php index 1394b0819095..b51b33558501 100644 --- a/system/ThirdParty/Kint/Utils.php +++ b/system/ThirdParty/Kint/Utils.php @@ -118,7 +118,9 @@ public static function composerGetExtras(string $key = 'kint'): array continue; } - foreach ($packages as $package) { + // Composer 2.0 Compatibility: packages are now wrapped into a "packages" top level key instead of the whole file being the package array + // @see https://getcomposer.org/upgrade/UPGRADE-2.0.md + foreach ($packages['packages'] ?? $packages as $package) { if (isset($package['extra'][$key]) && \is_array($package['extra'][$key])) { $extras = \array_replace($extras, $package['extra'][$key]); } From 82ddac7bba7691d6afbbb5bf069829df690559ea Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 12:43:55 +0900 Subject: [PATCH 12/26] docs: add how to download previous version user guide --- user_guide_src/source/installation/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/index.rst b/user_guide_src/source/installation/index.rst index 3be12a637dcc..8b71ea2833b0 100644 --- a/user_guide_src/source/installation/index.rst +++ b/user_guide_src/source/installation/index.rst @@ -10,8 +10,11 @@ Which is right for you? - If you would like the simple "download & go" install that CodeIgniter3 is known for, choose the manual installation. -However you choose to install and run CodeIgniter4, the +However you choose to install and run CodeIgniter4, the latest `user guide `_ is accessible online. +If you want to see previous versions, you can download from the +`codeigniter4/userguide `_ +repository. .. note:: Before using CodeIgniter 4, make sure that your server meets the :doc:`requirements `, in particular the PHP From 5c86c5c74c349094714ecc797323baaf159b1385 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:21:32 +0900 Subject: [PATCH 13/26] docs: add versionadded --- .../source/database/query_builder.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 709e55838941..a6c9f94444a0 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -908,6 +908,8 @@ Upsert $builder->upsert() ------------------ +.. versionadded:: 4.3.0 + Generates an upsert string based on the data you supply, and runs the query. You can either pass an **array** or an **object** to the method. By default a constraint will be defined in order. A primary @@ -929,6 +931,8 @@ The first parameter is an object. $builder->getCompiledUpsert() ----------------------------- +.. versionadded:: 4.3.0 + Compiles the upsert query just like ``$builder->upsert()`` but does not *run* the query. This method simply returns the SQL query as a string. @@ -944,6 +948,8 @@ upsertBatch $builder->upsertBatch() ----------------------- +.. versionadded:: 4.3.0 + Generates an upsert string based on the data you supply, and runs the query. You can either pass an **array** or an **object** to the method. By default a constraint will be defined in order. A primary @@ -967,6 +973,8 @@ You can also upsert from a query: $builder->onConstraint() ------------------------ +.. versionadded:: 4.3.0 + Allows manually setting constraint to be used for upsert. This does not work with MySQL because MySQL checks all constraints by default. @@ -976,6 +984,9 @@ This method accepts a string or an array of columns. $builder->updateFields() ------------------------ + +.. versionadded:: 4.3.0 + Allows manually setting the fields to be updated when performing upserts. .. literalinclude:: query_builder/110.php @@ -1146,6 +1157,8 @@ method, or ``emptyTable()``. $builder->deleteBatch() ----------------------- +.. versionadded:: 4.3.0 + Generates a batch **DELETE** statement based on a set of data. .. literalinclude:: query_builder/118.php @@ -1198,6 +1211,8 @@ When $builder->when() ---------------- +.. versionadded:: 4.3.0 + This allows modifying the query based on a condition without breaking out of the query builder chain. The first parameter is the condition, and it should evaluate to a boolean. The second parameter is a callable that will be ran @@ -1223,6 +1238,8 @@ WhenNot $builder->whenNot() ------------------- +.. versionadded:: 4.3.0 + This works exactly the same way as ``$builder->when()`` except that it will only run the callable when the condition evaluates to ``false``, instead of ``true`` like ``when()``. @@ -1415,6 +1432,8 @@ Class Reference .. php:method:: setQueryAsData($query[, $alias[, $columns = null]]) + .. versionadded:: 4.3.0 + :param BaseBuilder|RawSql $query: Instance of the BaseBuilder or RawSql :param string|null $alias: Alias for query :param array|string|null $columns: Array or comma delimited string of columns in the query From 6c956b06115f7d3b82993ba99d4eb4ab81c994b2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:21:58 +0900 Subject: [PATCH 14/26] docs: replace Mysql with MySQL --- user_guide_src/source/database/query_builder.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index a6c9f94444a0..02a336cc558a 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -953,7 +953,7 @@ $builder->upsertBatch() Generates an upsert string based on the data you supply, and runs the query. You can either pass an **array** or an **object** to the method. By default a constraint will be defined in order. A primary -key will be selected first and then unique keys. Mysql will use any +key will be selected first and then unique keys. MySQL will use any constraint by default. Here is an example using an array: .. literalinclude:: query_builder/108.php From 7e8b0869f9034f023b743338b834f36cc80afcda Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:23:15 +0900 Subject: [PATCH 15/26] docs: add notes --- user_guide_src/source/database/query_builder.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 02a336cc558a..6b66317dfe86 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1095,12 +1095,17 @@ UpdateBatch $builder->updateBatch() ----------------------- +.. note:: Since v4.3.0, the second parameter ``$index`` of ``updateBatch()`` has + changed to ``$constraints``. It now accepts types array, string, or ``RawSql``. + Generates an update string based on the data you supply, and runs the query. You can either pass an **array** or an **object** to the method. Here is an example using an array: .. literalinclude:: query_builder/092.php +.. note:: Since v4.3.0, the generated SQL structure has been Improved. + The first parameter is an associative array of values, the second parameter is the where key. .. note:: All values except ``RawSql`` are escaped automatically producing safer queries. From ec20625a15278211655f1ef7b523eb2ba0bc2d4e Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:23:34 +0900 Subject: [PATCH 16/26] docs: update notes --- user_guide_src/source/database/query_builder.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 6b66317dfe86..8fdb3dfaabee 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -966,7 +966,8 @@ You can also upsert from a query: .. literalinclude:: query_builder/115.php -.. note:: ``setQueryAsData()`` can be used since v4.3.0. +.. note:: The ``setQueryAsData()``, ``onConstraint()``, and ``updateFields()`` + methods can be used since v4.3.0. .. note:: It is required to alias the columns of the select query to match those of the target table. @@ -1120,7 +1121,8 @@ You can also update from a query: .. literalinclude:: query_builder/116.php -.. note:: ``setQueryAsData()`` can be used since v4.3.0. +.. note:: The ``setQueryAsData()``, ``onConstraint()``, and ``updateFields()`` + methods can be used since v4.3.0. .. note:: It is required to alias the columns of the select query to match those of the target table. From cd4cc572aaeb4b6e93961615fe2022cde1bfd8eb Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:24:04 +0900 Subject: [PATCH 17/26] docs: split sample code and add explanation --- .../source/database/query_builder.rst | 5 +++ .../source/database/query_builder/092.php | 24 +------------ .../source/database/query_builder/120.php | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 user_guide_src/source/database/query_builder/120.php diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 8fdb3dfaabee..56035153db80 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1109,6 +1109,11 @@ Here is an example using an array: The first parameter is an associative array of values, the second parameter is the where key. +Since v4.3.0, you can also use the ``setQueryAsData()``, ``onConstraint()``, and +``updateFields()`` methods: + +.. literalinclude:: query_builder/120.php + .. note:: All values except ``RawSql`` are escaped automatically producing safer queries. .. warning:: When you use ``RawSql``, you MUST escape the data manually. Failure to do so could result in SQL injections. diff --git a/user_guide_src/source/database/query_builder/092.php b/user_guide_src/source/database/query_builder/092.php index 911771fc1850..407eadbfef29 100644 --- a/user_guide_src/source/database/query_builder/092.php +++ b/user_guide_src/source/database/query_builder/092.php @@ -14,28 +14,7 @@ 'date' => 'Date 2', ], ]; - $builder->updateBatch($data, ['title', 'author']); - -// OR -$builder->setData($data)->onConstraint('title, author')->updateBatch(); - -// OR -$builder->setData($data, null, 'u') - ->onConstraint(['`mytable`.`title`' => '`u`.`title`', 'author' => new RawSql('`u`.`author`')]) - ->updateBatch(); - -// OR -foreach ($data as $row) { - $builder->setData($row); -} -$builder->onConstraint('title, author')->updateBatch(); - -// OR -$builder->setData($data, true, 'u') - ->onConstraint(new RawSql('`mytable`.`title` = `u`.`title` AND `mytable`.`author` = `u`.`author`')) - ->updateFields(['last_update' => new RawSql('CURRENT_TIMESTAMP()')], true) - ->updateBatch(); /* * Produces: * UPDATE `mytable` @@ -47,6 +26,5 @@ * SET * `mytable`.`title` = `u`.`title`, * `mytable`.`name` = `u`.`name`, - * `mytable`.`date` = `u`.`date`, - * `mytable`.`last_update` = CURRENT_TIMESTAMP() // this only applies to the last scenario + * `mytable`.`date` = `u`.`date` */ diff --git a/user_guide_src/source/database/query_builder/120.php b/user_guide_src/source/database/query_builder/120.php new file mode 100644 index 000000000000..d0bea8a1cfba --- /dev/null +++ b/user_guide_src/source/database/query_builder/120.php @@ -0,0 +1,36 @@ +setData($data)->onConstraint('title, author')->updateBatch(); + +// OR +$builder->setData($data, null, 'u') + ->onConstraint(['`mytable`.`title`' => '`u`.`title`', 'author' => new RawSql('`u`.`author`')]) + ->updateBatch(); + +// OR +foreach ($data as $row) { + $builder->setData($row); +} +$builder->onConstraint('title, author')->updateBatch(); + +// OR +$builder->setData($data, true, 'u') + ->onConstraint(new RawSql('`mytable`.`title` = `u`.`title` AND `mytable`.`author` = `u`.`author`')) + ->updateFields(['last_update' => new RawSql('CURRENT_TIMESTAMP()')], true) + ->updateBatch(); +/* + * Produces: + * UPDATE `mytable` + * INNER JOIN ( + * SELECT 'Title 1' `title`, 'Author 1' `author`, 'Name 1' `name`, 'Date 1' `date` UNION ALL + * SELECT 'Title 2' `title`, 'Author 2' `author`, 'Name 2' `name`, 'Date 2' `date` + * ) `u` + * ON `mytable`.`title` = `u`.`title` AND `mytable`.`author` = `u`.`author` + * SET + * `mytable`.`title` = `u`.`title`, + * `mytable`.`name` = `u`.`name`, + * `mytable`.`date` = `u`.`date`, + * `mytable`.`last_update` = CURRENT_TIMESTAMP() // this only applies to the last scenario + */ From 09276f8bf54b7d5c66a5283d5ea252db152df5e7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:25:03 +0900 Subject: [PATCH 18/26] docs: break long line in sample code --- user_guide_src/source/database/query_builder/115.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/database/query_builder/115.php b/user_guide_src/source/database/query_builder/115.php index cc5607d3982c..7526474da334 100644 --- a/user_guide_src/source/database/query_builder/115.php +++ b/user_guide_src/source/database/query_builder/115.php @@ -7,7 +7,10 @@ $additionalUpdateField = ['updated_at' => new RawSql('CURRENT_TIMESTAMP')]; -$sql = $builder->setQueryAsData($query)->onConstraint('email')->updateFields($additionalUpdateField, true)->upsertBatch(); +$sql = $builder->setQueryAsData($query) + ->onConstraint('email') + ->updateFields($additionalUpdateField, true) + ->upsertBatch(); /* MySQLi produces: INSERT INTO `db_user` (`country`, `email`, `name`) SELECT user2.name, user2.email, user2.country From 1450be7e1c470ba350c781a606ce523016f12485 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 13:30:11 +0900 Subject: [PATCH 19/26] chore: update Kint version in composer.json --- admin/framework/composer.json | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 248eea0c96cc..2f3022a92b2b 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -13,7 +13,7 @@ "psr/log": "^1.1" }, "require-dev": { - "kint-php/kint": "^5.0.1", + "kint-php/kint": "^5.0.3", "codeigniter/coding-standard": "^1.5", "fakerphp/faker": "^1.9", "friendsofphp/php-cs-fixer": "3.13.0", diff --git a/composer.json b/composer.json index 1fbc0fff4537..f4c9c31236fa 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "psr/log": "^1.1" }, "require-dev": { - "kint-php/kint": "^5.0.1", + "kint-php/kint": "^5.0.3", "codeigniter/coding-standard": "^1.5", "fakerphp/faker": "^1.9", "friendsofphp/php-cs-fixer": "3.13.0", From 09e267db52ddf02183f53af28ad072b8fc0f0c4d Mon Sep 17 00:00:00 2001 From: Samuel Asor Date: Sun, 5 Feb 2023 09:52:45 +0100 Subject: [PATCH 20/26] add support for display of error message using wildcard (*) --- system/Helpers/form_helper.php | 9 ++++++--- tests/system/Helpers/FormHelperTest.php | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index 57d3f38b63a4..dfd4a64f0669 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -741,9 +741,12 @@ function validation_show_error(string $field, string $template = 'single'): stri $config = config('Validation'); $view = Services::renderer(); - $errors = validation_errors(); + $errors = array_filter(validation_errors(), static fn ($key) => preg_match( + '/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/', + $key + ), ARRAY_FILTER_USE_KEY); - if (! array_key_exists($field, $errors)) { + if ($errors === []) { return ''; } @@ -751,7 +754,7 @@ function validation_show_error(string $field, string $template = 'single'): stri throw ValidationException::forInvalidTemplate($template); } - return $view->setVar('error', $errors[$field]) + return $view->setVar('error', implode("\n", $errors)) ->render($config->templates[$template]); } } diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 4e1ea582dd2d..d6508d8ff18c 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -996,6 +996,16 @@ public function testValidationShowError() $this->assertSame('The ID field is required.' . "\n", $html); } + public function testValidationShowErrorForWildcards() + { + $validation = Services::validation(); + $validation->setRule('user.0.name', 'Name', 'required')->run([]); + + $html = validation_show_error('user.*.name'); + + $this->assertSame('The Name field is required.' . "\n", $html); + } + public function testFormParseFormAttributesTrue() { $expected = 'readonly '; From f557414433b71210007cf533c907df67c8ebcffa Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Feb 2023 19:21:05 +0900 Subject: [PATCH 21/26] docs: add section "GPG-Signing Old Commits" All topics related to GPG signatures can be reached from this document. --- contributing/signing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributing/signing.md b/contributing/signing.md index e925327e9b71..6626911591d2 100644 --- a/contributing/signing.md +++ b/contributing/signing.md @@ -53,3 +53,7 @@ bash shell to use the **-S** option to force the secure signing. Regardless of how you sign a commit, commit messages are important too. See [Contribution Workflow](./workflow.md#commit-messages) for details. + +## GPG-Signing Old Commits + +See [Contribution Workflow](./workflow.md#gpg-signing-old-commits). From ce1247d7b05af5e52c13401c282dd095470a1181 Mon Sep 17 00:00:00 2001 From: Samuel Asor Date: Sun, 5 Feb 2023 12:15:23 +0100 Subject: [PATCH 22/26] applied code review suggestion --- tests/system/Helpers/FormHelperTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index d6508d8ff18c..0c914ee1df67 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -999,7 +999,16 @@ public function testValidationShowError() public function testValidationShowErrorForWildcards() { $validation = Services::validation(); - $validation->setRule('user.0.name', 'Name', 'required')->run([]); + $validation->setRule('user.*.name', 'Name', 'required') + ->run([ + 'user' => [ + 'friends' => [ + ['name' => 'Name1'], + ['name' => ''], + ['name' => 'Name2'], + ] + ] + ]); $html = validation_show_error('user.*.name'); From 256d9a2f6e5cb498bb98c82a79f65330980ef154 Mon Sep 17 00:00:00 2001 From: Samuel Asor Date: Sun, 5 Feb 2023 12:20:22 +0100 Subject: [PATCH 23/26] cs-fix --- tests/system/Helpers/FormHelperTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 0c914ee1df67..9a525adf7ad2 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -1006,8 +1006,8 @@ public function testValidationShowErrorForWildcards() ['name' => 'Name1'], ['name' => ''], ['name' => 'Name2'], - ] - ] + ], + ], ]); $html = validation_show_error('user.*.name'); From dbf23bf5d369cb809aa1820062ec2645001ffdf0 Mon Sep 17 00:00:00 2001 From: MichalKoder <50060787+MichalKoder@users.noreply.github.com> Date: Sun, 5 Feb 2023 14:07:16 +0100 Subject: [PATCH 24/26] A spelling correction in model.html --- user_guide_src/source/models/model.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 726c26dac728..06f7d84f26f2 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -727,7 +727,7 @@ Additionally, each model may allow (default) or deny callbacks class-wide by set .. literalinclude:: model/052.php -You may also change this setting temporarily for a single model call sing the ``allowCallbacks()`` method: +You may also change this setting temporarily for a single model call using the ``allowCallbacks()`` method: .. literalinclude:: model/053.php From 495418ae8efde7ba9a45967c3f3442d2a9b58f3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:01:21 +0000 Subject: [PATCH 25/26] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/0.15.11...0.15.12) --- updated-dependencies: - dependency-name: rector/rector dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f4c9c31236fa..8a2f047409f0 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "0.15.11", + "rector/rector": "0.15.12", "vimeo/psalm": "^5.0" }, "suggest": { From 20638680f388d753288146d839642276c72b9b32 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 7 Feb 2023 17:32:15 +0900 Subject: [PATCH 26/26] test: add comment --- tests/system/HTTP/IncomingRequestDetectingTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index f3be0aeb2247..cd220cbe0987 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -68,6 +68,14 @@ public function testPathRequestURI() public function testPathRequestURINested() { + // I'm not sure but this is a case of Apache config making such SERVER + // values? + // The current implementation doesn't use the value of the URI object. + // So I removed the code to set URI. Therefore, it's exactly the same as + // the method above as a test. + // But it may be changed in the future to use the value of the URI object. + // So I don't remove this test case. + // /ci/index.php/woot?code=good#pos $_SERVER['REQUEST_URI'] = '/index.php/woot'; $_SERVER['SCRIPT_NAME'] = '/index.php';