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..8a2f047409f0 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", @@ -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": { 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). 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 - 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)); } 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/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/system/Router/Router.php b/system/Router/Router.php index 137aa4e43af9..8dd35c656e39 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 @@ -162,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 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 */ 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]); } diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php index ab0bfb5c6092..cd220cbe0987 100644 --- a/tests/system/HTTP/IncomingRequestDetectingTest.php +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -31,127 +31,158 @@ 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() + public function testPathDefaultEmpty() { - $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'; + // 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'; - $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'; - $_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'; - $expected = 'ci/woot'; + + $expected = 'ci/woot'; + $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() { - $this->request->uri = '/?/ci/index.php/woot'; - $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + // /index.php? + $_SERVER['REQUEST_URI'] = '/index.php?'; $_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', ]); diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 4e1ea582dd2d..9a525adf7ad2 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -996,6 +996,25 @@ public function testValidationShowError() $this->assertSame('The ID field is required.' . "\n", $html); } + public function testValidationShowErrorForWildcards() + { + $validation = Services::validation(); + $validation->setRule('user.*.name', 'Name', 'required') + ->run([ + 'user' => [ + 'friends' => [ + ['name' => 'Name1'], + ['name' => ''], + ['name' => 'Name2'], + ], + ], + ]); + + $html = validation_show_error('user.*.name'); + + $this->assertSame('The Name field is required.' . "\n", $html); + } + public function testFormParseFormAttributesTrue() { $expected = 'readonly '; 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() diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 709e55838941..56035153db80 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,10 +948,12 @@ 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 -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 @@ -960,13 +966,16 @@ 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. $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 +985,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 @@ -1084,14 +1096,24 @@ 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. +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. @@ -1104,7 +1126,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. @@ -1146,6 +1169,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 +1223,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 +1250,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 +1444,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 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/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 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 + */ 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 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