Welcome to CodeIgniter = CodeIgniter\CodeIgniter::CI_VERSION ?>
+
+
The small framework with powerful features
+
+
@@ -231,91 +231,91 @@
-
About this page
+
About this page
-
The page you are looking at is being generated dynamically by CodeIgniter.
+
The page you are looking at is being generated dynamically by CodeIgniter.
-
If you would like to edit this page you will find it located at:
+
If you would like to edit this page you will find it located at:
-
app/Views/welcome_message.php
+
app/Views/welcome_message.php
-
The corresponding controller for this page can be found at:
+
The corresponding controller for this page can be found at:
-
app/Controllers/Home.php
+
app/Controllers/Home.php
-
+
-
Go further
+
Go further
-
-
- Learn
-
+
+
+ Learn
+
-
The User Guide contains an introduction, tutorial, a number of "how to"
- guides, and then reference documentation for the components that make up
- the framework. Check the User Guide !
+
The User Guide contains an introduction, tutorial, a number of "how to"
+ guides, and then reference documentation for the components that make up
+ the framework. Check the User Guide !
-
-
- Discuss
-
+
+
+ Discuss
+
-
CodeIgniter is a community-developed open source project, with several
- venues for the community members to gather and exchange ideas. View all
- the threads on CodeIgniter's forum, or chat on Slack !
+
CodeIgniter is a community-developed open source project, with several
+ venues for the community members to gather and exchange ideas. View all
+ the threads on CodeIgniter's forum, or chat on Slack !
-
-
- Contribute
-
+
+
+ Contribute
+
-
CodeIgniter is a community driven project and accepts contributions
- of code and documentation from the community. Why not
-
- join us ?
+
CodeIgniter is a community driven project and accepts contributions
+ of code and documentation from the community. Why not
+
+ join us ?
-
+
diff --git a/app/index.html b/app/index.html
index b702fbc3967b..69df4e1dff68 100644
--- a/app/index.html
+++ b/app/index.html
@@ -1,7 +1,7 @@
- 403 Forbidden
+ 403 Forbidden
diff --git a/composer.json b/composer.json
index 376e2cdff38f..2c93e30b7d72 100644
--- a/composer.json
+++ b/composer.json
@@ -5,26 +5,26 @@
"homepage": "https://codeigniter.com",
"license": "MIT",
"require": {
- "php": "^7.3 || ^8.0",
+ "php": "^7.4 || ^8.0",
"ext-curl": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "kint-php/kint": "^4.0",
+ "kint-php/kint": "^4.1.1",
"laminas/laminas-escaper": "^2.9",
"psr/log": "^1.1"
},
"require-dev": {
- "codeigniter/coding-standard": "1.2.*",
+ "codeigniter/coding-standard": "^1.1",
"fakerphp/faker": "^1.9",
- "friendsofphp/php-cs-fixer": "3.2.*",
+ "friendsofphp/php-cs-fixer": "3.6.*",
"mikey179/vfsstream": "^1.6",
"nexusphp/cs-config": "^3.3",
"nexusphp/tachycardia": "^1.0",
- "phpstan/phpstan": "1.4.3",
+ "phpstan/phpstan": "^1.7.1",
"phpunit/phpunit": "^9.1",
"predis/predis": "^1.1",
- "rector/rector": "0.12.10"
+ "rector/rector": "0.13.3"
},
"suggest": {
"ext-fileinfo": "Improves mime type detection for files"
@@ -50,6 +50,7 @@
"autoload-dev": {
"psr-4": {
"CodeIgniter\\": "tests/system/",
+ "CodeIgniter\\AutoReview\\": "tests/AutoReview/",
"Utils\\": "utils/"
}
},
@@ -61,12 +62,14 @@
"analyze": "phpstan analyse",
"test": "phpunit",
"cs": [
- "php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php",
- "php-cs-fixer fix --verbose --dry-run --diff"
+ "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php",
+ "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php",
+ "php-cs-fixer fix --ansi --verbose --dry-run --diff"
],
"cs-fix": [
- "php-cs-fixer fix --verbose --diff --config=.no-header.php-cs-fixer.dist.php",
- "php-cs-fixer fix --verbose --diff"
+ "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.user-guide.php",
+ "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php",
+ "php-cs-fixer fix --ansi --verbose --diff"
]
},
"scripts-descriptions": {
diff --git a/contributing/bug_report.md b/contributing/bug_report.md
index c564a9a734f4..43767cdd1766 100644
--- a/contributing/bug_report.md
+++ b/contributing/bug_report.md
@@ -27,15 +27,7 @@ have found a bug, again - please ask on the forums first.
## Security
-Did you find a security issue in CodeIgniter?
-
-Please *don't* disclose it publicly, but e-mail us at
-, or report it via our page on
-[HackerOne](https://hackerone.com/codeigniter).
-
-If you've found a critical vulnerability, we'd be happy to credit you in
-our
-[ChangeLog](https://codeigniter4.github.io/CodeIgniter4/changelogs/index.html).
+See [SECURITY.md](../SECURITY.md).
## Tips for a Good Issue Report
diff --git a/contributing/css.md b/contributing/css.md
index 3108319a0e1b..7accf5e83801 100644
--- a/contributing/css.md
+++ b/contributing/css.md
@@ -1,6 +1,6 @@
-# Contribution CSS
+# Contribution to Debug Toolbar CSS
-CodeIgniter uses SASS to generate the debug toolbar's CSS. Therefore,
+CodeIgniter uses Dart Sass to generate the debug toolbar's CSS. Therefore,
you will need to install it first. You can find further instructions on
the official website:
@@ -9,34 +9,32 @@ the official website:
Open your terminal, and navigate to CodeIgniter's root folder. To
generate the CSS file, use the following command:
-`sass --no-cache --sourcemap=none admin/css/debug-toolbar/toolbar.scss system/Debug/Toolbar/Views/toolbar.css`
+`sass --no-source-map admin/css/debug-toolbar/toolbar.scss system/Debug/Toolbar/Views/toolbar.css`
-Details:
-- `--no-cache` is a parameter defined to disable SASS cache,
-this prevents a "cache" folder from being created
-- `--sourcemap=none` is a parameter which prevents soucemap files from being generated
-- `admin/css/debug-toolbar/toolbar.scss` is the SASS source
+Details:
+- `--no-source-map` is an option which prevents sourcemap files from being generated
+- `admin/css/debug-toolbar/toolbar.scss` is the SASS source
- `system/Debug/Toolbar/Views/toolbar.css` is he CSS destination
## Color scheme
**Themes**
-Dark: `#252525` / `rgb(37, 37, 37)`
-Light: `#FFFFFF` / `rgb(255, 255, 255)`
+Dark: `#252525` / `rgb(37, 37, 37)`
+Light: `#FFFFFF` / `rgb(255, 255, 255)`
**Glossy colors**
-Blue: `#5BC0DE` / `rgb(91, 192, 222)`
-Gray: `#434343` / `rgb(67, 67, 67)`
-Green: `#9ACE25` / `rgb(154, 206, 37)`
-Orange: `#DD8615` / `rgb(221, 134, 21)`
-Red: `#DD4814` / `rgb(221, 72, 20)`
+Blue: `#5BC0DE` / `rgb(91, 192, 222)`
+Gray: `#434343` / `rgb(67, 67, 67)`
+Green: `#9ACE25` / `rgb(154, 206, 37)`
+Orange: `#DD8615` / `rgb(221, 134, 21)`
+Red: `#DD4814` / `rgb(221, 72, 20)`
**Matt colors**
-Blue: `#D8EAF0` / `rgb(216, 234, 240)`
-Gray: `#DFDFDF` / `rgb(223, 223, 223)`
-Green: `#DFF0D8` / `rgb(223, 240, 216)`
-Orange: `#FDC894` / `rgb(253, 200, 148)`
-Red: `#EF9090` / `rgb(239, 144, 144)`
+Blue: `#D8EAF0` / `rgb(216, 234, 240)`
+Gray: `#DFDFDF` / `rgb(223, 223, 223)`
+Green: `#DFF0D8` / `rgb(223, 240, 216)`
+Orange: `#FDC894` / `rgb(253, 200, 148)`
+Red: `#EF9090` / `rgb(239, 144, 144)`
diff --git a/contributing/documentation.rst b/contributing/documentation.rst
index eba3eb99437e..405cb127902a 100644
--- a/contributing/documentation.rst
+++ b/contributing/documentation.rst
@@ -9,9 +9,7 @@ on readability and user friendliness.
While they can be quite technical, we always write for humans!
A local table of contents should always be included, like the one below.
-It is created automatically by inserting the following:
-
-::
+It is created automatically by inserting the following::
.. contents::
:local:
@@ -86,8 +84,8 @@ create these with the following tab triggers::
References
**********
-References to a Section
-=======================
+To a Section
+============
If you need to link to a specific section, the first you add the label before a header::
@@ -102,11 +100,30 @@ And then you can reference it like this::
See :ref:`curlrequest-request-options-headers` for how to add.
-References to a Page
-====================
+To a Section in the Page
+========================
+
+You can reference a section in the current page like the following::
+
+ See `Result Rows`_
+
+To a Page
+=========
You can reference a page like the following::
- :doc:`Session <../libraries/sessions>` library
+ See :doc:`Session <../libraries/sessions>` library
+
+ See :doc:`../libraries/sessions` library
+
+To a URL
+========
+
+ `CodeIgniter 4 framework `_
+
+To a Function
+=============
+
+ :php:func:`dot_array_search`
- :doc:`../libraries/sessions` library
+ :php:func:`Response::setCookie() `
diff --git a/contributing/internals.md b/contributing/internals.md
index 6ced5f56dc48..e2d387028fe1 100644
--- a/contributing/internals.md
+++ b/contributing/internals.md
@@ -17,11 +17,9 @@ other core packages, you can create that in the constructor using the
override that:
```php
- public function __construct(Foo $foo=null)
+ public function __construct(?Foo $foo = null)
{
- $this->foo = $foo instanceOf Foo
- ? $foo
- : \Config\Services::foo();
+ $this->foo = $foo ?? \Config\Services::foo();
}
```
@@ -75,7 +73,7 @@ package itself will need its own sub-namespace that collects all related
files into one grouping, like `CodeIgniter\HTTP`.
Files MUST be named the same as the class they hold, and they must match
-the Style Guide <./styleguide.md>, meaning CamelCase class and
+the [Style Guide](styleguide.md), meaning CamelCase class and
file names. They should be in their own directory that matches the
sub-namespace under the **system** directory.
@@ -122,17 +120,9 @@ scans and keep performance high.
## Command-Line Support
-CodeIgniter has never been known for it's strong CLI support. However,
+CodeIgniter has never been known for its strong CLI support. However,
if your package could benefit from it, create a new file under
-**system/Commands**. The class contained within is simply a controller
-that is intended for CLI usage only. The `index()` method should provide
-a list of available commands provided by that package.
-
-Routes must be added to **system/Config/Routes.php** using the `cli()`
-method to ensure it is not accessible through the browser, but is
-restricted to the CLI only.
-
-See the **MigrationsCommand** file for an example.
+**system/Commands**.
## Documentation
diff --git a/contributing/pull_request.md b/contributing/pull_request.md
index e788b76fede3..51f976232987 100644
--- a/contributing/pull_request.md
+++ b/contributing/pull_request.md
@@ -119,7 +119,7 @@ See [Contribution CSS](./css.md).
### Compatibility
-CodeIgniter4 requires [PHP 7.3](https://php.net/releases/7_3_0.php).
+CodeIgniter4 requires [PHP 7.4](https://php.net/releases/7_4_0.php).
### Backwards Compatibility
diff --git a/contributing/workflow.md b/contributing/workflow.md
index b2309df9477a..c4c4bb073865 100644
--- a/contributing/workflow.md
+++ b/contributing/workflow.md
@@ -78,7 +78,7 @@ Then synchronizing is done by pulling from us and pushing to you. This
is normally done locally, so that you can resolve any merge conflicts.
For instance, to synchronize **develop** branches:
- git checkout develop
+ git switch develop
git fetch upstream
git merge upstream/develop
git push origin develop
@@ -109,8 +109,8 @@ For instance, make sure you are in the *develop* branch, and create a
new feature branch, based on *develop*, for a new feature you are
creating:
- git checkout develop
- git checkout -b new/mind-reader
+ git switch develop
+ git switch -c new/mind-reader
Saving changes only updates your local working area.
@@ -131,15 +131,15 @@ Just make sure that your commits in a feature branch are all related.
If you are working on two features at a time, then you will want to
switch between them to keep the contributions separate. For instance:
- git checkout new/mind-reader
+ git switch new/mind-reader
// work away
git add .
git commit -S -m "Added adapter for abc"
- git checkout fix/issue-123
+ git switch fix/issue-123
// work away
git add .
git commit -S -m "Fixed problem in DEF\Something"
- git checkout develop
+ git switch develop
The last checkout makes sure that you end up in your *develop* branch as
a starting point for your next session working with your repository.
@@ -155,17 +155,16 @@ that it could benefit from a review by fellow developers.
> Remember to sync your local repo with the shared one before pushing!
It is a lot easier to resolve conflicts at this stage.
-
Synchronize your repository:
- git checkout develop
+ git switch develop
git fetch upstream
git merge upstream/develop
git push origin develop
Bring your feature branch up to date:
- git checkout new/mind-reader
+ git switch new/mind-reader
git rebase upstream/develop
And finally push your local branch to your GitHub repository:
@@ -215,6 +214,33 @@ Label your PRs with the one of the following [labels](https://github.com/codeign
And if your PRs have the breaking changes, label the following label:
- **breaking change** ... PRs that may break existing functionalities
+## Updating Your Branch
+
+If you are asked for changes in the review, commit the fix in your branch and push it to GitHub again.
+
+If the `develop` branch progresses and conflicts arise that prevent merging, or if you are asked to *rebase*,
+do the following:
+
+Synchronize your repository:
+
+ git switch develop
+ git fetch upstream
+ git merge upstream/develop
+ git push origin develop
+
+Bring your feature branch up to date:
+
+ git switch new/mind-reader
+ git rebase upstream/develop
+
+You might get conflicts when you rebase. It is your
+responsibility to resolve those locally, so that you can continue
+collaborating with the shared repository.
+
+And finally push your local branch to your GitHub repository:
+
+ git push --force-with-lease origin new/mind-reader
+
## Cleanup
If your PR is accepted and merged into the shared repository, you can
diff --git a/depfile.yaml b/depfile.yaml
deleted file mode 100644
index 301f17076f82..000000000000
--- a/depfile.yaml
+++ /dev/null
@@ -1,231 +0,0 @@
-# Defines the layers for each framework
-# component and their allowed interactions.
-# The following components are exempt
-# due to their global nature:
-# - CLI & Commands
-# - Config
-# - Debug
-# - Exception
-# - Service
-# - Validation\FormatRules
-paths:
- - ./app
- - ./system
-exclude_files:
- - '#.*test.*#i'
-layers:
- - name: API
- collectors:
- - type: className
- regex: ^Codeigniter\\API\\.*
- - name: Cache
- collectors:
- - type: className
- regex: ^Codeigniter\\Cache\\.*
- - name: Controller
- collectors:
- - type: className
- regex: ^CodeIgniter\\Controller$
- - name: Cookie
- collectors:
- - type: className
- regex: ^Codeigniter\\Cookie\\.*
- - name: Database
- collectors:
- - type: className
- regex: ^Codeigniter\\Database\\.*
- - name: Email
- collectors:
- - type: className
- regex: ^Codeigniter\\Email\\.*
- - name: Encryption
- collectors:
- - type: className
- regex: ^Codeigniter\\Encryption\\.*
- - name: Entity
- collectors:
- - type: className
- regex: ^Codeigniter\\Entity\\.*
- - name: Events
- collectors:
- - type: className
- regex: ^Codeigniter\\Events\\.*
- - name: Files
- collectors:
- - type: className
- regex: ^Codeigniter\\Files\\.*
- - name: Filters
- collectors:
- - type: bool
- must:
- - type: className
- regex: ^Codeigniter\\Filters\\Filter.*
- - name: Format
- collectors:
- - type: className
- regex: ^Codeigniter\\Format\\.*
- - name: Honeypot
- collectors:
- - type: className
- regex: ^Codeigniter\\.*Honeypot.* # includes the Filter
- - name: HTTP
- collectors:
- - type: bool
- must:
- - type: className
- regex: ^Codeigniter\\HTTP\\.*
- must_not:
- - type: className
- regex: (Exception|URI)
- - name: I18n
- collectors:
- - type: className
- regex: ^Codeigniter\\I18n\\.*
- - name: Images
- collectors:
- - type: className
- regex: ^Codeigniter\\Images\\.*
- - name: Language
- collectors:
- - type: className
- regex: ^Codeigniter\\Language\\.*
- - name: Log
- collectors:
- - type: className
- regex: ^Codeigniter\\Log\\.*
- - name: Model
- collectors:
- - type: className
- regex: ^Codeigniter\\.*Model$
- - name: Modules
- collectors:
- - type: className
- regex: ^Codeigniter\\Modules\\.*
- - name: Pager
- collectors:
- - type: className
- regex: ^Codeigniter\\Pager\\.*
- - name: Publisher
- collectors:
- - type: className
- regex: ^Codeigniter\\Publisher\\.*
- - name: RESTful
- collectors:
- - type: className
- regex: ^Codeigniter\\RESTful\\.*
- - name: Router
- collectors:
- - type: className
- regex: ^Codeigniter\\Router\\.*
- - name: Security
- collectors:
- - type: className
- regex: ^Codeigniter\\Security\\.*
- - name: Session
- collectors:
- - type: className
- regex: ^Codeigniter\\Session\\.*
- - name: Throttle
- collectors:
- - type: className
- regex: ^Codeigniter\\Throttle\\.*
- - name: Typography
- collectors:
- - type: className
- regex: ^Codeigniter\\Typography\\.*
- - name: URI
- collectors:
- - type: className
- regex: ^CodeIgniter\\HTTP\\URI$
- - name: Validation
- collectors:
- - type: bool
- must:
- - type: className
- regex: ^Codeigniter\\Validation\\.*
- must_not:
- - type: className
- regex: ^Codeigniter\\Validation\\FormatRules$
- - name: View
- collectors:
- - type: className
- regex: ^Codeigniter\\View\\.*
-ruleset:
- API:
- - Format
- - HTTP
- Controller:
- - HTTP
- - Validation
- Database:
- - Entity
- - Events
- Email:
- - Events
- Entity:
- - I18n
- Filters:
- - HTTP
- Honeypot:
- - Filters
- - HTTP
- HTTP:
- - Cookie
- - Files
- - Security
- - URI
- Images:
- - Files
- Model:
- - Database
- - I18n
- - Pager
- - Validation
- Pager:
- - URI
- - View
- Publisher:
- - Files
- - URI
- RESTful:
- - +API
- - +Controller
- Router:
- - HTTP
- Security:
- - Cookie
- - Session
- - HTTP
- Session:
- - Cookie
- - Database
- Throttle:
- - Cache
- Validation:
- - HTTP
- View:
- - Cache
-skip_violations:
- # Individual class exemptions
- CodeIgniter\Entity\Cast\URICast:
- - CodeIgniter\HTTP\URI
- CodeIgniter\Log\Handlers\ChromeLoggerHandler:
- - CodeIgniter\HTTP\ResponseInterface
- CodeIgniter\View\Table:
- - CodeIgniter\Database\BaseResult
- CodeIgniter\View\Plugins:
- - CodeIgniter\HTTP\URI
-
- # BC changes that should be fixed
- CodeIgniter\HTTP\ResponseTrait:
- - CodeIgniter\Pager\PagerInterface
- CodeIgniter\HTTP\ResponseInterface:
- - CodeIgniter\Pager\PagerInterface
- CodeIgniter\HTTP\Response:
- - CodeIgniter\Pager\PagerInterface
- CodeIgniter\HTTP\RedirectResponse:
- - CodeIgniter\Pager\PagerInterface
- CodeIgniter\HTTP\DownloadResponse:
- - CodeIgniter\Pager\PagerInterface
- CodeIgniter\Validation\Validation:
- - CodeIgniter\View\RendererInterface
diff --git a/deptrac.yaml b/deptrac.yaml
new file mode 100644
index 000000000000..8d43af6117ae
--- /dev/null
+++ b/deptrac.yaml
@@ -0,0 +1,233 @@
+# Defines the layers for each framework
+# component and their allowed interactions.
+# The following components are exempt
+# due to their global nature:
+# - CLI & Commands
+# - Config
+# - Debug
+# - Exception
+# - Service
+# - Validation\FormatRules
+parameters:
+ paths:
+ - ./app
+ - ./system
+ exclude_files:
+ - '#.*test.*#i'
+ layers:
+ - name: API
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\API\\.*
+ - name: Cache
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Cache\\.*
+ - name: Controller
+ collectors:
+ - type: className
+ regex: ^CodeIgniter\\Controller$
+ - name: Cookie
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Cookie\\.*
+ - name: Database
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Database\\.*
+ - name: Email
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Email\\.*
+ - name: Encryption
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Encryption\\.*
+ - name: Entity
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Entity\\.*
+ - name: Events
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Events\\.*
+ - name: Files
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Files\\.*
+ - name: Filters
+ collectors:
+ - type: bool
+ must:
+ - type: className
+ regex: ^Codeigniter\\Filters\\Filter.*
+ - name: Format
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Format\\.*
+ - name: Honeypot
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\.*Honeypot.* # includes the Filter
+ - name: HTTP
+ collectors:
+ - type: bool
+ must:
+ - type: className
+ regex: ^Codeigniter\\HTTP\\.*
+ must_not:
+ - type: className
+ regex: (Exception|URI)
+ - name: I18n
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\I18n\\.*
+ - name: Images
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Images\\.*
+ - name: Language
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Language\\.*
+ - name: Log
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Log\\.*
+ - name: Model
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\.*Model$
+ - name: Modules
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Modules\\.*
+ - name: Pager
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Pager\\.*
+ - name: Publisher
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Publisher\\.*
+ - name: RESTful
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\RESTful\\.*
+ - name: Router
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Router\\.*
+ - name: Security
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Security\\.*
+ - name: Session
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Session\\.*
+ - name: Throttle
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Throttle\\.*
+ - name: Typography
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\Typography\\.*
+ - name: URI
+ collectors:
+ - type: className
+ regex: ^CodeIgniter\\HTTP\\URI$
+ - name: Validation
+ collectors:
+ - type: bool
+ must:
+ - type: className
+ regex: ^Codeigniter\\Validation\\.*
+ must_not:
+ - type: className
+ regex: ^Codeigniter\\Validation\\FormatRules$
+ - name: View
+ collectors:
+ - type: className
+ regex: ^Codeigniter\\View\\.*
+ ruleset:
+ API:
+ - Format
+ - HTTP
+ Controller:
+ - HTTP
+ - Validation
+ Database:
+ - Entity
+ - Events
+ Email:
+ - Events
+ Entity:
+ - I18n
+ Filters:
+ - HTTP
+ Honeypot:
+ - Filters
+ - HTTP
+ HTTP:
+ - Cookie
+ - Files
+ - Security
+ - URI
+ Images:
+ - Files
+ Model:
+ - Database
+ - I18n
+ - Pager
+ - Validation
+ Pager:
+ - URI
+ - View
+ Publisher:
+ - Files
+ - URI
+ RESTful:
+ - +API
+ - +Controller
+ Router:
+ - HTTP
+ Security:
+ - Cookie
+ - Session
+ - HTTP
+ Session:
+ - Cookie
+ - HTTP
+ - Database
+ Throttle:
+ - Cache
+ Validation:
+ - HTTP
+ View:
+ - Cache
+ skip_violations:
+ # Individual class exemptions
+ CodeIgniter\Entity\Cast\URICast:
+ - CodeIgniter\HTTP\URI
+ CodeIgniter\Log\Handlers\ChromeLoggerHandler:
+ - CodeIgniter\HTTP\ResponseInterface
+ CodeIgniter\View\Table:
+ - CodeIgniter\Database\BaseResult
+ CodeIgniter\View\Plugins:
+ - CodeIgniter\HTTP\URI
+
+ # BC changes that should be fixed
+ CodeIgniter\HTTP\ResponseTrait:
+ - CodeIgniter\Pager\PagerInterface
+ CodeIgniter\HTTP\ResponseInterface:
+ - CodeIgniter\Pager\PagerInterface
+ CodeIgniter\HTTP\Response:
+ - CodeIgniter\Pager\PagerInterface
+ CodeIgniter\HTTP\RedirectResponse:
+ - CodeIgniter\Pager\PagerInterface
+ CodeIgniter\HTTP\DownloadResponse:
+ - CodeIgniter\Pager\PagerInterface
+ CodeIgniter\Validation\Validation:
+ - CodeIgniter\View\RendererInterface
diff --git a/env b/env
index c60b367265e7..67faaee5b57a 100644
--- a/env
+++ b/env
@@ -21,6 +21,8 @@
#--------------------------------------------------------------------
# app.baseURL = ''
+# If you have trouble with `.`, you could also use `_`.
+# app_baseURL = ''
# app.forceGlobalSecureRequests = false
# app.sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'
@@ -60,7 +62,7 @@
# contentsecuritypolicy.scriptSrc = 'self'
# contentsecuritypolicy.styleSrc = 'self'
# contentsecuritypolicy.imageSrc = 'self'
-# contentsecuritypolicy.base_uri = null
+# contentsecuritypolicy.baseURI = null
# contentsecuritypolicy.childSrc = null
# contentsecuritypolicy.connectSrc = 'self'
# contentsecuritypolicy.fontSrc = null
@@ -73,6 +75,9 @@
# contentsecuritypolicy.reportURI = null
# contentsecuritypolicy.sandbox = false
# contentsecuritypolicy.upgradeInsecureRequests = false
+# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
+# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
+# contentsecuritypolicy.autoNonce = true
#--------------------------------------------------------------------
# COOKIE
diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist
index d25e5e23a409..17f71d164f29 100644
--- a/phpstan-baseline.neon.dist
+++ b/phpstan-baseline.neon.dist
@@ -25,11 +25,6 @@ parameters:
count: 1
path: system/Autoloader/Autoloader.php
- -
- message: "#^Method CodeIgniter\\\\Validation\\\\ValidationInterface\\:\\:run\\(\\) invoked with 3 parameters, 0\\-2 required\\.$#"
- count: 1
- path: system/BaseModel.php
-
-
message: "#^Property Config\\\\Cache\\:\\:\\$backupHandler \\(string\\) in isset\\(\\) is not nullable\\.$#"
count: 1
@@ -105,11 +100,6 @@ parameters:
count: 1
path: system/CodeIgniter.php
- -
- message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:getSegments\\(\\)\\.$#"
- count: 1
- path: system/CodeIgniter.php
-
-
message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:setLocale\\(\\)\\.$#"
count: 1
@@ -125,21 +115,6 @@ parameters:
count: 1
path: system/CodeIgniter.php
- -
- message: "#^Unreachable statement \\- code above always terminates\\.$#"
- count: 1
- path: system/CodeIgniter.php
-
- -
- message: "#^Binary operation \"\\+\" between array\\\\|false and non\\-empty\\-array\\ results in an error\\.$#"
- count: 1
- path: system/Common.php
-
- -
- message: "#^Variable \\$params on left side of \\?\\? always exists and is not nullable\\.$#"
- count: 1
- path: system/Common.php
-
-
message: "#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$db \\(CodeIgniter\\\\Database\\\\BaseConnection\\) in empty\\(\\) is not falsy\\.$#"
count: 1
@@ -205,11 +180,6 @@ parameters:
count: 1
path: system/Database/MigrationRunner.php
- -
- message: "#^Cannot access property \\$affected_rows on bool\\|object\\|resource\\.$#"
- count: 1
- path: system/Database/MySQLi/Connection.php
-
-
message: "#^Cannot access property \\$errno on bool\\|object\\|resource\\.$#"
count: 1
@@ -485,16 +455,6 @@ parameters:
count: 1
path: system/Debug/Exceptions.php
- -
- message: "#^Parameter \\#4 \\$replacement of function array_splice expects array\\|string, true given\\.$#"
- count: 1
- path: system/Debug/Exceptions.php
-
- -
- message: "#^Property CodeIgniter\\\\Debug\\\\Exceptions\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Debug/Exceptions.php
-
-
message: "#^Property Config\\\\Exceptions\\:\\:\\$sensitiveDataInTrace \\(array\\) in isset\\(\\) is not nullable\\.$#"
count: 1
@@ -505,16 +465,6 @@ parameters:
count: 1
path: system/Debug/Toolbar.php
- -
- message: "#^Variable \\$request on left side of \\?\\? always exists and is not nullable\\.$#"
- count: 1
- path: system/Debug/Toolbar.php
-
- -
- message: "#^Variable \\$response on left side of \\?\\? always exists and is not nullable\\.$#"
- count: 1
- path: system/Debug/Toolbar.php
-
-
message: "#^Call to an undefined method CodeIgniter\\\\View\\\\RendererInterface\\:\\:getPerformanceData\\(\\)\\.$#"
count: 1
@@ -570,11 +520,6 @@ parameters:
count: 1
path: system/Filters/Filters.php
- -
- message: "#^Parameter \\#1 \\$seconds of function sleep expects int, float given\\.$#"
- count: 1
- path: system/HTTP/CURLRequest.php
-
-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
@@ -621,12 +566,7 @@ parameters:
path: system/HTTP/Request.php
-
- message: "#^Property Config\\\\App\\:\\:\\$cookieSameSite \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 3
- path: system/HTTP/Response.php
-
- -
- message: "#^Cannot unset offset 'path' on array\\{host\\: mixed\\}\\.$#"
+ message: "#^Cannot unset offset 'path' on array{host: non-empty-string}\\.$#"
count: 1
path: system/HTTP/URI.php
@@ -660,16 +600,6 @@ parameters:
count: 1
path: system/Helpers/number_helper.php
- -
- message: "#^Variable \\$mockService in empty\\(\\) always exists and is always falsy\\.$#"
- count: 1
- path: system/Helpers/test_helper.php
-
- -
- message: "#^Parameter \\#2 \\$times of function str_repeat expects int, float given\\.$#"
- count: 1
- path: system/Helpers/text_helper.php
-
-
message: "#^Variable \\$pool might not be defined\\.$#"
count: 2
@@ -725,19 +655,9 @@ parameters:
count: 1
path: system/Log/Logger.php
- -
- message: "#^Call to an undefined method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:asArray\\(\\)\\.$#"
- count: 1
- path: system/Model.php
-
- -
- message: "#^Property CodeIgniter\\\\RESTful\\\\ResourceController\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/RESTful/ResourceController.php
-
-
message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getDefaultNamespace\\(\\)\\.$#"
- count: 2
+ count: 3
path: system/Router/Router.php
-
@@ -752,7 +672,7 @@ parameters:
-
message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutesOptions\\(\\)\\.$#"
- count: 2
+ count: 1
path: system/Router/Router.php
-
@@ -766,100 +686,30 @@ parameters:
path: system/Router/Router.php
-
- message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Router/Router.php
-
- -
- message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(.*\\)\\.$#"
count: 2
path: system/Router/Router.php
-
- message: "#^Property Config\\\\App\\:\\:\\$CSRFCookieName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\App\\:\\:\\$CSRFExpire \\(int\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\App\\:\\:\\$CSRFHeaderName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\App\\:\\:\\$CSRFRegenerate \\(bool\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\App\\:\\:\\$CSRFTokenName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\Security\\:\\:\\$cookieName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\Security\\:\\:\\$csrfProtection \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\Security\\:\\:\\$expires \\(int\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
- path: system/Security/Security.php
-
- -
- message: "#^Property Config\\\\Security\\:\\:\\$headerName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
+ message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
- path: system/Security/Security.php
+ path: system/Router/Router.php
-
- message: "#^Property Config\\\\Security\\:\\:\\$regenerate \\(bool\\) on left side of \\?\\? is not nullable\\.$#"
+ message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 1
- path: system/Security/Security.php
+ path: system/Router/Router.php
-
- message: "#^Property Config\\\\Security\\:\\:\\$tokenName \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
+ message: "#^Property Config\\\\App\\:\\:\\$CSRF[a-zA-Z]+ \\([a-zA-Z]+\\) on left side of \\?\\? is not nullable\\.$#"
+ count: 6
path: system/Security/Security.php
-
- message: "#^Property Config\\\\Security\\:\\:\\$tokenRandomize \\(bool\\) on left side of \\?\\? is not nullable\\.$#"
- count: 1
+ message: "#^Property Config\\\\Security\\:\\:\\$[a-zA-Z]+ \\([a-zA-Z]+\\) on left side of \\?\\? is not nullable\\.$#"
+ count: 8
path: system/Security/Security.php
- -
- message: "#^Access to an undefined property Config\\\\App\\:\\:\\$sessionDBGroup\\.$#"
- count: 1
- path: system/Session/Handlers/DatabaseHandler.php
-
- -
- message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Session/Handlers/DatabaseHandler.php
-
- -
- message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Session/Handlers/FileHandler.php
-
- -
- message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Session/Handlers/MemcachedHandler.php
-
- -
- message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Session/Handlers/RedisHandler.php
-
-
message: "#^Strict comparison using \\=\\=\\= between string and true will always evaluate to false\\.$#"
count: 1
@@ -880,11 +730,6 @@ parameters:
count: 1
path: system/Session/Session.php
- -
- message: "#^Property Config\\\\App\\:\\:\\$cookieSameSite \\(string\\) on left side of \\?\\? is not nullable\\.$#"
- count: 2
- path: system/Session/Session.php
-
-
message: "#^Property Config\\\\App\\:\\:\\$cookieSecure \\(bool\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
@@ -960,11 +805,6 @@ parameters:
count: 1
path: system/Test/Fabricator.php
- -
- message: "#^Access to protected property CodeIgniter\\\\HTTP\\\\Request\\:\\:\\$uri\\.$#"
- count: 1
- path: system/Test/FeatureTestCase.php
-
-
message: "#^Property CodeIgniter\\\\Test\\\\CIUnitTestCase\\:\\:\\$bodyFormat \\(string\\) in isset\\(\\) is not nullable\\.$#"
count: 1
@@ -985,11 +825,6 @@ parameters:
count: 1
path: system/Test/Mock/MockConnection.php
- -
- message: "#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockResourcePresenter\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#"
- count: 1
- path: system/Test/Mock/MockResourcePresenter.php
-
-
message: "#^Property CodeIgniter\\\\Throttle\\\\Throttler\\:\\:\\$testTime \\(int\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
@@ -1015,3 +850,11 @@ parameters:
count: 1
path: system/View/Parser.php
+ -
+ message: "#^Result of \\|\\| is always false\\.$#"
+ paths:
+ - system/Cache/CacheFactory.php
+
+ -
+ message: "#^Binary operation \"/\" between string and 8 results in an error\\.$#"
+ path: system/Encryption/Handlers/OpenSSLHandler.php
diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php
new file mode 100644
index 000000000000..99acf96227fe
--- /dev/null
+++ b/phpstan-bootstrap.php
@@ -0,0 +1,7 @@
+
+
+ ./tests/AutoReview
+ ./tests/system./tests/system/Database
diff --git a/preload.php b/preload.php
new file mode 100644
index 000000000000..7e1a04956fa9
--- /dev/null
+++ b/preload.php
@@ -0,0 +1,113 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+/*
+ *---------------------------------------------------------------
+ * Sample file for Preloading
+ *---------------------------------------------------------------
+ * See https://www.php.net/manual/en/opcache.preloading.php
+ *
+ * How to Use:
+ * 1. Set Preload::$paths.
+ * 2. Set opcache.preload in php.ini.
+ * php.ini:
+ * opcache.preload=/path/to/preload.php
+ */
+
+// Load the paths config file
+require __DIR__ . '/app/Config/Paths.php';
+
+// Path to the front controller
+define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
+
+/**
+ * See https://www.php.net/manual/en/function.str-contains.php#126277
+ */
+if (! function_exists('str_contains')) {
+ /**
+ * Polyfill of str_contains()
+ */
+ function str_contains(string $haystack, string $needle): bool
+ {
+ return empty($needle) || strpos($haystack, $needle) !== false;
+ }
+}
+
+class Preload
+{
+ /**
+ * @var array Paths to preload.
+ */
+ private array $paths = [
+ [
+ 'include' => // __DIR__ . '/vendor/codeigniter4/framework/system',
+ __DIR__ . '/system',
+ 'exclude' => [
+ // Not needed if you don't use them.
+ '/system/Database/OCI8/',
+ '/system/Database/Postgre/',
+ '/system/Database/SQLSRV/',
+ // Not needed.
+ '/system/Database/Seeder.php',
+ '/system/Test/',
+ '/system/Language/',
+ '/system/CLI/',
+ '/system/Commands/',
+ '/system/Publisher/',
+ '/system/ComposerScripts.php',
+ '/Views/',
+ // Errors occur.
+ '/system/Config/Routes.php',
+ '/system/ThirdParty/',
+ ],
+ ],
+ ];
+
+ public function __construct()
+ {
+ $this->loadAutoloader();
+ }
+
+ private function loadAutoloader()
+ {
+ $paths = new Config\Paths();
+ require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
+ }
+
+ /**
+ * Load PHP files.
+ */
+ public function load()
+ {
+ foreach ($this->paths as $path) {
+ $directory = new RecursiveDirectoryIterator($path['include']);
+ $fullTree = new RecursiveIteratorIterator($directory);
+ $phpFiles = new RegexIterator(
+ $fullTree,
+ '/.+((? $file) {
+ foreach ($path['exclude'] as $exclude) {
+ if (str_contains($file[0], $exclude)) {
+ continue 2;
+ }
+ }
+
+ require_once $file[0];
+ echo 'Loaded: ' . $file[0] . "\n";
+ }
+ }
+ }
+}
+
+(new Preload())->load();
diff --git a/public/index.php b/public/index.php
index 77373025f96d..51f4be81e731 100644
--- a/public/index.php
+++ b/public/index.php
@@ -3,6 +3,9 @@
// Path to the front controller (this file)
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR);
+// Ensure the current directory is pointing to the front controller's directory
+chdir(FCPATH);
+
/*
*---------------------------------------------------------------
* BOOTSTRAP THE APPLICATION
@@ -12,20 +15,34 @@
* and fires up an environment-specific bootstrapping.
*/
-// Ensure the current directory is pointing to the front controller's directory
-chdir(__DIR__);
-
// Load our paths config file
// This is the line that might need to be changed, depending on your folder structure.
-$pathsConfig = FCPATH . '../app/Config/Paths.php';
-// ^^^ Change this if you move your application folder
-require realpath($pathsConfig) ?: $pathsConfig;
+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.
-$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
-$app = require realpath($bootstrap) ?: $bootstrap;
+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();
+
+/*
+ * ---------------------------------------------------------------
+ * GRAB OUR CODEIGNITER INSTANCE
+ * ---------------------------------------------------------------
+ *
+ * The CodeIgniter class contains the core functionality to make
+ * the application run, and does all of 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);
/*
*---------------------------------------------------------------
@@ -34,4 +51,5 @@
* Now that everything is setup, it's time to actually fire
* up the engines and make this app do its thang.
*/
+
$app->run();
diff --git a/rector.php b/rector.php
index 21f63c0eca1b..e83bfcc18c63 100644
--- a/rector.php
+++ b/rector.php
@@ -17,19 +17,17 @@
use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector;
+use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector;
use Rector\CodeQuality\Rector\If_\CombineIfRector;
use Rector\CodeQuality\Rector\If_\ShortenElseIfRector;
use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector;
use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector;
-use Rector\CodeQuality\Rector\Return_\SimplifyUselessVariableRector;
use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector;
use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector;
use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector;
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
-use Rector\Core\Configuration\Option;
-use Rector\Core\ValueObject\PhpVersion;
+use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
-use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector;
use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
@@ -43,48 +41,46 @@
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
+use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
-use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Utils\Rector\PassStrictParameterToFunctionParameterRector;
use Utils\Rector\RemoveErrorSuppressInTryCatchStmtsRector;
use Utils\Rector\RemoveVarTagFromClassConstantRector;
use Utils\Rector\UnderscoreToCamelCaseVariableNameRector;
-return static function (ContainerConfigurator $containerConfigurator): void {
- $containerConfigurator->import(SetList::DEAD_CODE);
- $containerConfigurator->import(LevelSetList::UP_TO_PHP_73);
- $containerConfigurator->import(PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD);
- $containerConfigurator->import(PHPUnitSetList::PHPUNIT_80);
+return static function (RectorConfig $rectorConfig): void {
+ $rectorConfig->sets([
+ SetList::DEAD_CODE,
+ LevelSetList::UP_TO_PHP_74,
+ PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD,
+ PHPUnitSetList::PHPUNIT_80,
+ PHPUnitSetList::REMOVE_MOCKS,
+ ]);
- $parameters = $containerConfigurator->parameters();
+ $rectorConfig->parallel();
- $parameters->set(Option::PARALLEL, true);
// paths to refactor; solid alternative to CLI arguments
- $parameters->set(Option::PATHS, [__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils/Rector']);
+ $rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils/Rector']);
// do you need to include constants, class aliases or custom autoloader? files listed will be executed
- $parameters->set(Option::BOOTSTRAP_FILES, [
+ $rectorConfig->bootstrapFiles([
__DIR__ . '/system/Test/bootstrap.php',
]);
// is there a file you need to skip?
- $parameters->set(Option::SKIP, [
+ $rectorConfig->skip([
__DIR__ . '/app/Views',
__DIR__ . '/system/Debug/Toolbar/Views/toolbar.tpl.php',
- __DIR__ . '/system/Debug/Kint/RichRenderer.php',
__DIR__ . '/system/ThirdParty',
__DIR__ . '/tests/system/Config/fixtures',
__DIR__ . '/tests/_support',
JsonThrowOnErrorRector::class,
StringifyStrNeedlesRector::class,
- // requires php 8
- RemoveUnusedPromotedPropertyRector::class,
-
- // private method called via getPrivateMethodInvoker
RemoveUnusedPrivateMethodRector::class => [
+ // private method called via getPrivateMethodInvoker
__DIR__ . '/tests/system/Test/ReflectionHelperTest.php',
],
@@ -103,9 +99,18 @@
__DIR__ . '/system/Session/Handlers',
],
- // may cause load view files directly when detecting class that
- // make warning
- StringClassNameToClassConstantRector::class,
+ StringClassNameToClassConstantRector::class => [
+ // may cause load view files directly when detecting namespaced string
+ // due to internal PHPStan issue
+ __DIR__ . '/app/Config/Pager.php',
+ __DIR__ . '/app/Config/Validation.php',
+ __DIR__ . '/tests/system/Validation/StrictRules/ValidationTest.php',
+ __DIR__ . '/tests/system/Validation/ValidationTest.php',
+
+ // expected Qualified name
+ __DIR__ . '/tests/system/Autoloader/FileLocatorTest.php',
+ __DIR__ . '/tests/system/Router/RouteCollectionTest.php',
+ ],
// sometime too detail
CountOnNullRector::class,
@@ -118,34 +123,41 @@
]);
// auto import fully qualified class names
- $parameters->set(Option::AUTO_IMPORT_NAMES, true);
- $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_73);
-
- $services = $containerConfigurator->services();
- $services->set(UnderscoreToCamelCaseVariableNameRector::class);
- $services->set(SimplifyUselessVariableRector::class);
- $services->set(RemoveAlwaysElseRector::class);
- $services->set(PassStrictParameterToFunctionParameterRector::class);
- $services->set(CountArrayToEmptyArrayComparisonRector::class);
- $services->set(ForToForeachRector::class);
- $services->set(ChangeNestedForeachIfsToEarlyContinueRector::class);
- $services->set(ChangeIfElseValueAssignToEarlyReturnRector::class);
- $services->set(SimplifyStrposLowerRector::class);
- $services->set(CombineIfRector::class);
- $services->set(SimplifyIfReturnBoolRector::class);
- $services->set(InlineIfToExplicitIfRector::class);
- $services->set(PreparedValueToEarlyReturnRector::class);
- $services->set(ShortenElseIfRector::class);
- $services->set(SimplifyIfElseToTernaryRector::class);
- $services->set(UnusedForeachValueToArrayKeysRector::class);
- $services->set(ChangeArrayPushToArrayAssignRector::class);
- $services->set(UnnecessaryTernaryExpressionRector::class);
- $services->set(RemoveErrorSuppressInTryCatchStmtsRector::class);
- $services->set(RemoveVarTagFromClassConstantRector::class);
- $services->set(AddPregQuoteDelimiterRector::class);
- $services->set(SimplifyRegexPatternRector::class);
- $services->set(FuncGetArgsToVariadicParamRector::class);
- $services->set(MakeInheritedMethodVisibilitySameAsParentRector::class);
- $services->set(SimplifyEmptyArrayCheckRector::class);
- $services->set(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
+ $rectorConfig->importNames();
+
+ $rectorConfig->rule(UnderscoreToCamelCaseVariableNameRector::class);
+ $rectorConfig->rule(SimplifyUselessVariableRector::class);
+ $rectorConfig->rule(RemoveAlwaysElseRector::class);
+ $rectorConfig->rule(PassStrictParameterToFunctionParameterRector::class);
+ $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
+ $rectorConfig->rule(ForToForeachRector::class);
+ $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
+ $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
+ $rectorConfig->rule(SimplifyStrposLowerRector::class);
+ $rectorConfig->rule(CombineIfRector::class);
+ $rectorConfig->rule(SimplifyIfReturnBoolRector::class);
+ $rectorConfig->rule(InlineIfToExplicitIfRector::class);
+ $rectorConfig->rule(PreparedValueToEarlyReturnRector::class);
+ $rectorConfig->rule(ShortenElseIfRector::class);
+ $rectorConfig->rule(SimplifyIfElseToTernaryRector::class);
+ $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
+ $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
+ $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
+ $rectorConfig->rule(RemoveErrorSuppressInTryCatchStmtsRector::class);
+ $rectorConfig->rule(RemoveVarTagFromClassConstantRector::class);
+ $rectorConfig->rule(AddPregQuoteDelimiterRector::class);
+ $rectorConfig->rule(SimplifyRegexPatternRector::class);
+ $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
+ $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
+ $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
+ $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
+ $rectorConfig->ruleWithConfiguration(StringClassNameToClassConstantRector::class, [
+ 'Error',
+ 'Exception',
+ 'InvalidArgumentException',
+ 'Closure',
+ 'stdClass',
+ 'SQLite3',
+ ]);
+ $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class);
};
diff --git a/spark b/spark
index 9a5a5db90284..225422aace74 100755
--- a/spark
+++ b/spark
@@ -1,6 +1,15 @@
#!/usr/bin/env php
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
/*
* --------------------------------------------------------------------
* CodeIgniter command-line tools
@@ -12,8 +21,28 @@
* this class mainly acts as a passthru to the framework itself.
*/
+// Refuse to run when called from php-cgi
+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");
+}
+
+// We want errors to be shown when using it from the CLI.
+error_reporting(-1);
+ini_set('display_errors', '1');
+
+/**
+ * @var bool
+ *
+ * @deprecated No longer in use. `CodeIgniter` has `$context` property.
+ */
define('SPARKED', true);
+// Path to the front controller
+define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
+
+// Ensure the current directory is pointing to the front controller's directory
+chdir(FCPATH);
+
/*
*---------------------------------------------------------------
* BOOTSTRAP THE APPLICATION
@@ -23,34 +52,28 @@ define('SPARKED', true);
* and fires up an environment-specific bootstrapping.
*/
-// Refuse to run when called from php-cgi
-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");
-}
-
-// Path to the front controller
-define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
-
// Load our paths config file
-$pathsConfig = 'app/Config/Paths.php';
+// 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
-require realpath($pathsConfig) ?: $pathsConfig;
$paths = new Config\Paths();
-// Ensure the current directory is pointing to the front controller's directory
-chdir(FCPATH);
+// 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();
-$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
-$app = require realpath($bootstrap) ?: $bootstrap;
+// Grab our CodeIgniter
+$app = Config\Services::codeigniter();
+$app->initialize();
+$app->setContext('spark');
// Grab our Console
$console = new CodeIgniter\CLI\Console($app);
-// We want errors to be shown when using it from the CLI.
-error_reporting(-1);
-ini_set('display_errors', '1');
-
// Show basic information before we do anything else.
if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) {
unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore
diff --git a/stale.yml b/stale.yml
deleted file mode 100644
index 897cc082a10e..000000000000
--- a/stale.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 60
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
-# Issues with these labels will never be considered stale
-exemptLabels:
- - pinned
- - security
-# Label to use when marking an issue as stale
-staleLabel: wontfix
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be automatically closed in a week if no further activity occurs.
- Thank you for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php
index 9ee722b45235..f20affaffaf5 100644
--- a/system/API/ResponseTrait.php
+++ b/system/API/ResponseTrait.php
@@ -75,7 +75,7 @@ trait ResponseTrait
/**
* Current Formatter instance. This is usually set by ResponseTrait::format
*
- * @var FormatterInterface
+ * @var FormatterInterface|null
*/
protected $formatter;
diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php
index 492fb09930f0..311428fe4165 100644
--- a/system/Autoloader/Autoloader.php
+++ b/system/Autoloader/Autoloader.php
@@ -82,6 +82,10 @@ class Autoloader
*/
public function initialize(Autoload $config, Modules $modules)
{
+ $this->prefixes = [];
+ $this->classmap = [];
+ $this->files = [];
+
// We have to have one or the other, though we don't enforce the need
// to have both present in order to work.
if (empty($config->psr4) && empty($config->classmap)) {
@@ -100,12 +104,28 @@ public function initialize(Autoload $config, Modules $modules)
$this->files = $config->files;
}
+ if (is_file(COMPOSER_PATH)) {
+ $this->loadComposerInfo($modules);
+ }
+
+ return $this;
+ }
+
+ private function loadComposerInfo(Modules $modules): void
+ {
+ /**
+ * @var ClassLoader $composer
+ */
+ $composer = include COMPOSER_PATH;
+
+ $this->loadComposerClassmap($composer);
+
// Should we load through Composer's namespaces, also?
if ($modules->discoverInComposer) {
- $this->discoverComposerNamespaces();
+ $this->loadComposerNamespaces($composer);
}
- return $this;
+ unset($composer);
}
/**
@@ -292,8 +312,36 @@ public function sanitizeFilename(string $filename): string
return trim($filename, '.-_');
}
+ private function loadComposerNamespaces(ClassLoader $composer): void
+ {
+ $paths = $composer->getPrefixesPsr4();
+
+ // Get rid of CodeIgniter so we don't have duplicates
+ if (isset($paths['CodeIgniter\\'])) {
+ unset($paths['CodeIgniter\\']);
+ }
+
+ $newPaths = [];
+
+ foreach ($paths as $key => $value) {
+ // Composer stores namespaces with trailing slash. We don't.
+ $newPaths[rtrim($key, '\\ ')] = $value;
+ }
+
+ $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.
+ *
+ * @deprecated No longer used.
*/
protected function discoverComposerNamespaces()
{
diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php
index b8bfdf6df217..14d7982c49b4 100644
--- a/system/Autoloader/FileLocator.php
+++ b/system/Autoloader/FileLocator.php
@@ -108,7 +108,7 @@ public function locateFile(string $file, ?string $folder = null, string $ext = '
}
/**
- * Examines a file and returns the fully qualified domain name.
+ * Examines a file and returns the fully qualified class name.
*/
public function getClassname(string $file): string
{
@@ -186,7 +186,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp =
}
if (! $prioritizeApp && ! empty($appPaths)) {
- $foundPaths = array_merge($foundPaths, $appPaths);
+ $foundPaths = [...$foundPaths, ...$appPaths];
}
// Remove any duplicates
@@ -212,7 +212,7 @@ protected function ensureExt(string $path, string $ext): string
/**
* Return the namespace mappings we know about.
*
- * @return array|string
+ * @return array>
*/
protected function getNamespaces()
{
@@ -289,6 +289,8 @@ 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 string[] List of file paths
*/
public function listFiles(string $path): array
{
@@ -307,7 +309,7 @@ public function listFiles(string $path): array
continue;
}
- $tempFiles = get_filenames($fullPath, true);
+ $tempFiles = get_filenames($fullPath, true, false, false);
if (! empty($tempFiles)) {
$files = array_merge($files, $tempFiles);
@@ -319,7 +321,9 @@ public function listFiles(string $path): array
/**
* Scans the provided namespace, returning a list of all files
- * that are contained within the subpath specified by $path.
+ * that are contained within the sub path specified by $path.
+ *
+ * @return string[] List of file paths
*/
public function listNamespaceFiles(string $prefix, string $path): array
{
@@ -339,7 +343,7 @@ public function listNamespaceFiles(string $prefix, string $path): array
continue;
}
- $tempFiles = get_filenames($fullPath, true);
+ $tempFiles = get_filenames($fullPath, true, false, false);
if (! empty($tempFiles)) {
$files = array_merge($files, $tempFiles);
diff --git a/system/BaseModel.php b/system/BaseModel.php
index 971303664a8e..39bd9ce2cb4c 100644
--- a/system/BaseModel.php
+++ b/system/BaseModel.php
@@ -293,9 +293,9 @@ public function __construct(?ValidationInterface $validation = null)
$this->tempAllowCallbacks = $this->allowCallbacks;
/**
- * @var Validation $validation
+ * @var Validation|null $validation
*/
- $validation = $validation ?? Services::validation(null, false);
+ $validation ??= Services::validation(null, false);
$this->validation = $validation;
$this->initialize();
@@ -1076,7 +1076,7 @@ public function paginate(?int $perPage = null, string $group = 'default', ?int $
$pager = Services::pager(null, null, false);
if ($segment) {
- $pager->setSegment($segment);
+ $pager->setSegment($segment, $group);
}
$page = $page >= 1 ? $page : $pager->getCurrentPage($group);
@@ -1344,7 +1344,9 @@ public function validate($data): bool
return true;
}
- return $this->validation->setRules($rules, $this->validationMessages)->run($data, null, $this->DBGroup);
+ $this->validation->reset()->setRules($rules, $this->validationMessages);
+
+ return $this->validation->run($data, null, $this->DBGroup);
}
/**
@@ -1489,9 +1491,9 @@ public function asObject(string $class = 'object')
}
/**
- * Takes a class an returns an array of it's public and protected
+ * Takes a class and returns an array of it's public and protected
* properties as an array suitable for use in creates and updates.
- * This method use objectToRawArray internally and does conversion
+ * This method uses objectToRawArray() internally and does conversion
* to string on all Time instances
*
* @param object|string $data Data
@@ -1521,7 +1523,7 @@ protected function objectToArray($data, bool $onlyChanged = true, bool $recursiv
}
/**
- * Takes a class an returns an array of it's public and protected
+ * Takes a class and returns an array of its public and protected
* properties as an array with raw values.
*
* @param object|string $data Data
diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php
index 8f843c4f5c73..f5d0d370e0ea 100644
--- a/system/CLI/BaseCommand.php
+++ b/system/CLI/BaseCommand.php
@@ -96,7 +96,7 @@ public function __construct(LoggerInterface $logger, Commands $commands)
/**
* Actually execute a command.
*
- * @param array $params
+ * @param array $params
*/
abstract public function run(array $params);
diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php
index 347a894f7578..d88374e80944 100644
--- a/system/CLI/CLI.php
+++ b/system/CLI/CLI.php
@@ -469,7 +469,7 @@ public static function clearScreen()
*/
public static function color(string $text, string $foreground, ?string $background = null, ?string $format = null): string
{
- if (! static::$isColored) {
+ if (! static::$isColored || $text === '') {
return $text;
}
@@ -481,6 +481,48 @@ public static function color(string $text, string $foreground, ?string $backgrou
throw CLIException::forInvalidColor('background', $background);
}
+ $newText = '';
+
+ // Detect if color method was already in use with this text
+ if (strpos($text, "\033[0m") !== false) {
+ $pattern = '/\\033\\[0;.+?\\033\\[0m/u';
+
+ preg_match_all($pattern, $text, $matches);
+ $coloredStrings = $matches[0];
+
+ // No colored string found. Invalid strings with no `\033[0;??`.
+ if ($coloredStrings === []) {
+ return $newText . self::getColoredText($text, $foreground, $background, $format);
+ }
+
+ $nonColoredText = preg_replace(
+ $pattern,
+ '<<__colored_string__>>',
+ $text
+ );
+ $nonColoredChunks = preg_split(
+ '/<<__colored_string__>>/u',
+ $nonColoredText
+ );
+
+ foreach ($nonColoredChunks as $i => $chunk) {
+ if ($chunk !== '') {
+ $newText .= self::getColoredText($chunk, $foreground, $background, $format);
+ }
+
+ if (isset($coloredStrings[$i])) {
+ $newText .= $coloredStrings[$i];
+ }
+ }
+ } else {
+ $newText .= self::getColoredText($text, $foreground, $background, $format);
+ }
+
+ return $newText;
+ }
+
+ private static function getColoredText(string $text, string $foreground, ?string $background, ?string $format): string
+ {
$string = "\033[" . static::$foreground_colors[$foreground] . 'm';
if ($background !== null) {
@@ -491,30 +533,6 @@ public static function color(string $text, string $foreground, ?string $backgrou
$string .= "\033[4m";
}
- // Detect if color method was already in use with this text
- if (strpos($text, "\033[0m") !== false) {
- // Split the text into parts so that we can see
- // if any part missing the color definition
- $chunks = mb_split('\\033\\[0m', $text);
- // Reset text
- $text = '';
-
- foreach ($chunks as $chunk) {
- if ($chunk === '') {
- continue;
- }
-
- // If chunk doesn't have colors defined we need to add them
- if (strpos($chunk, "\033[") === false) {
- $chunk = static::color($chunk, $foreground, $background, $format);
- // Add color reset before chunk and clear end of the string
- $text .= rtrim("\033[0m" . $chunk, "\033[0m");
- } else {
- $text .= $chunk;
- }
- }
- }
-
return $string . $text . "\033[0m";
}
diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php
index a6985d0db931..ef4ed057b606 100644
--- a/system/CLI/CommandRunner.php
+++ b/system/CLI/CommandRunner.php
@@ -40,19 +40,14 @@ public function __construct()
* so we have the chance to look for a Command first.
*
* @param string $method
- * @param array ...$params
+ * @param array $params
*
* @throws ReflectionException
*
* @return mixed
*/
- public function _remap($method, ...$params)
+ public function _remap($method, $params)
{
- // The first param is usually empty, so scrap it.
- if (empty($params[0])) {
- array_shift($params);
- }
-
return $this->index($params);
}
diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php
index 3f84f33d918f..2714db30bcf9 100644
--- a/system/CLI/Commands.php
+++ b/system/CLI/Commands.php
@@ -96,9 +96,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->findQualifiedNameFromPath($file);
+ $className = $locator->getClassname($file);
- if (empty($className) || ! class_exists($className)) {
+ if ($className === '' || ! class_exists($className)) {
continue;
}
diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php
index 5d1f37cdfc24..87ab4e333dca 100644
--- a/system/Cache/Handlers/PredisHandler.php
+++ b/system/Cache/Handlers/PredisHandler.php
@@ -222,6 +222,6 @@ public function getMetaData(string $key)
*/
public function isSupported(): bool
{
- return class_exists('Predis\Client');
+ return class_exists(Client::class);
}
}
diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php
index 5704e57be92e..82b08e318879 100644
--- a/system/CodeIgniter.php
+++ b/system/CodeIgniter.php
@@ -12,7 +12,6 @@
namespace CodeIgniter;
use Closure;
-use CodeIgniter\Debug\Kint\RichRenderer;
use CodeIgniter\Debug\Timer;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
@@ -30,10 +29,13 @@
use CodeIgniter\Router\Router;
use Config\App;
use Config\Cache;
+use Config\Kint as KintConfig;
use Config\Services;
use Exception;
use Kint;
use Kint\Renderer\CliRenderer;
+use Kint\Renderer\RichRenderer;
+use LogicException;
/**
* This class is the core of the framework, and will analyse the
@@ -45,9 +47,9 @@ class CodeIgniter
/**
* The current version of CodeIgniter Framework
*/
- public const CI_VERSION = '4.1.9';
+ public const CI_VERSION = '4.2.0';
- private const MIN_PHP_VERSION = '7.3';
+ private const MIN_PHP_VERSION = '7.4';
/**
* App startup time.
@@ -80,7 +82,7 @@ class CodeIgniter
/**
* Current request.
*
- * @var CLIRequest|IncomingRequest|Request
+ * @var CLIRequest|IncomingRequest|Request|null
*/
protected $request;
@@ -141,6 +143,16 @@ class CodeIgniter
*/
protected $useSafeOutput = false;
+ /**
+ * Context
+ * web: Invoked by HTTP request
+ * php-cli: Invoked by CLI via `php public/index.php`
+ * spark: Invoked by CLI via the `spark` command
+ *
+ * @phpstan-var 'php-cli'|'spark'|'web'
+ */
+ protected ?string $context = null;
+
/**
* Constructor.
*/
@@ -236,7 +248,7 @@ protected function initializeKint()
$file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
- if (file_exists($file)) {
+ if (is_file($file)) {
require_once $file;
}
});
@@ -247,7 +259,7 @@ protected function initializeKint()
/**
* Config\Kint
*/
- $config = config('Config\Kint');
+ $config = config(KintConfig::class);
Kint::$depth_limit = $config->maxDepth;
Kint::$display_called_from = $config->displayCalledFrom;
@@ -257,7 +269,11 @@ protected function initializeKint()
Kint::$plugins = $config->plugins;
}
- Kint::$renderers[Kint::MODE_RICH] = RichRenderer::class;
+ $csp = Services::csp();
+ if ($csp->enabled()) {
+ RichRenderer::$js_nonce = $csp->getScriptNonce();
+ RichRenderer::$css_nonce = $csp->getStyleNonce();
+ }
RichRenderer::$theme = $config->richTheme;
RichRenderer::$folder = $config->richFolder;
@@ -286,10 +302,14 @@ protected function initializeKint()
* @throws Exception
* @throws RedirectException
*
- * @return bool|mixed|RequestInterface|ResponseInterface
+ * @return bool|mixed|RequestInterface|ResponseInterface|void
*/
public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
{
+ if ($this->context === null) {
+ throw new LogicException('Context must be set before run() is called. If you are upgrading from 4.1.x, you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.');
+ }
+
$this->startBenchmark();
$this->getRequestObject();
@@ -299,7 +319,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
$this->spoofRequestMethod();
- if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'cli') {
+ if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') {
$this->response->setStatusCode(405)->setBody('Method Not Allowed');
return $this->sendResponse();
@@ -322,6 +342,11 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
return;
}
+ // spark command has nothing to do with HTTP redirect and 404
+ if ($this->isSparked()) {
+ return $this->handleRequest($routes, $cacheConfig, $returnResponse);
+ }
+
try {
return $this->handleRequest($routes, $cacheConfig, $returnResponse);
} catch (RedirectException $e) {
@@ -355,6 +380,30 @@ public function useSafeOutput(bool $safe = true)
return $this;
}
+ /**
+ * Invoked via spark command?
+ */
+ private function isSparked(): bool
+ {
+ return $this->context === 'spark';
+ }
+
+ /**
+ * Invoked via php-cli command?
+ */
+ private function isPhpCli(): bool
+ {
+ return $this->context === 'php-cli';
+ }
+
+ /**
+ * Web access?
+ */
+ private function isWeb(): bool
+ {
+ return $this->context === 'web';
+ }
+
/**
* Handles the main request logic and fires the controller.
*
@@ -387,7 +436,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
}
// Never run filters when running through Spark cli
- if (! defined('SPARKED')) {
+ if (! $this->isSparked()) {
// Run "before" filters
$this->benchmark->start('before_filters');
$possibleResponse = $filters->run($uri, 'before');
@@ -428,7 +477,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
$this->gatherOutput($cacheConfig, $returned);
// Never run filters when running through Spark cli
- if (! defined('SPARKED')) {
+ if (! $this->isSparked()) {
$filters->setResponse($this->response);
// Run "after" filters
@@ -546,10 +595,8 @@ protected function getRequestObject()
return;
}
- if (is_cli() && ENVIRONMENT !== 'testing') {
- // @codeCoverageIgnoreStart
+ if ($this->isSparked() || $this->isPhpCli()) {
$this->request = Services::clirequest($this->config);
- // @codeCoverageIgnoreEnd
} else {
$this->request = Services::request($this->config);
// guess at protocol if needed
@@ -565,7 +612,7 @@ protected function getResponseObject()
{
$this->response = Services::response($this->config);
- if (! is_cli() || ENVIRONMENT === 'testing') {
+ if ($this->isWeb()) {
$this->response->setProtocolVersion($this->request->getProtocolVersion());
}
@@ -583,7 +630,7 @@ protected function getResponseObject()
* @param int $duration How long the Strict Transport Security
* should be enforced for this URL.
*/
- protected function forceSecureAccess($duration = 31536000)
+ protected function forceSecureAccess($duration = 31_536_000)
{
if ($this->config->forceGlobalSecureRequests !== true) {
return;
@@ -705,7 +752,7 @@ public function displayPerformanceMetrics(string $output): string
*
* @throws RedirectException
*
- * @return string|string[]|null
+ * @return string|string[]|null Route filters, that is, the filters specified in the routes file
*/
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
{
@@ -746,6 +793,8 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
/**
* Determines the path to use for us to try to route to, based
* on user input (setPath), or the CLI/IncomingRequest path.
+ *
+ * @return string
*/
protected function determinePath()
{
@@ -775,6 +824,8 @@ public function setPath(string $path)
* Now that everything has been setup, this method attempts to run the
* controller method and make the script go. If it's not able to, will
* show the appropriate Page Not Found error.
+ *
+ * @return ResponseInterface|string|void
*/
protected function startController()
{
@@ -802,7 +853,7 @@ protected function startController()
/**
* Instantiates the controller class.
*
- * @return mixed
+ * @return Controller
*/
protected function createController()
{
@@ -817,19 +868,34 @@ protected function createController()
/**
* Runs the controller, allowing for _remap methods to function.
*
+ * CI4 supports three types of requests:
+ * 1. Web: URI segments become parameters, sent to Controllers via Routes,
+ * output controlled by Headers to browser
+ * 2. Spark: accessed by CLI via the spark command, arguments are Command arguments,
+ * sent to Commands by CommandRunner, output controlled by CLI class
+ * 3. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
+ * sent to Controllers via Routes, output varies
+ *
* @param mixed $class
*
- * @return mixed
+ * @return false|ResponseInterface|string|void
*/
protected function runController($class)
{
- // If this is a console request then use the input segments as parameters
- $params = defined('SPARKED') ? $this->request->getSegments() : $this->router->params();
+ if ($this->isSparked()) {
+ // This is a Spark request
+ /** @var CLIRequest $request */
+ $request = $this->request;
+ $params = $request->getArgs();
- if (method_exists($class, '_remap')) {
- $output = $class->_remap($this->method, ...$params);
+ $output = $class->_remap($this->method, $params);
} else {
- $output = $class->{$this->method}(...$params);
+ // This is a Web request or PHP CLI request
+ $params = $this->router->params();
+
+ $output = method_exists($class, '_remap')
+ ? $class->_remap($this->method, ...$params)
+ : $class->{$this->method}(...$params);
}
$this->benchmark->stop('controller');
@@ -845,6 +911,8 @@ protected function display404errors(PageNotFoundException $e)
{
// Is there a 404 Override available?
if ($override = $this->router->get404Override()) {
+ $returned = null;
+
if ($override instanceof Closure) {
echo $override($e->getMessage());
} elseif (is_array($override)) {
@@ -855,13 +923,13 @@ protected function display404errors(PageNotFoundException $e)
$this->method = $override[1];
$controller = $this->createController();
- $this->runController($controller);
+ $returned = $this->runController($controller);
}
unset($override);
$cacheConfig = new Cache();
- $this->gatherOutput($cacheConfig);
+ $this->gatherOutput($cacheConfig, $returned);
$this->sendResponse();
return;
@@ -882,14 +950,16 @@ protected function display404errors(PageNotFoundException $e)
ob_end_flush(); // @codeCoverageIgnore
}
- throw PageNotFoundException::forPageNotFound(ENVIRONMENT !== 'production' || is_cli() ? $e->getMessage() : '');
+ throw PageNotFoundException::forPageNotFound(
+ (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : ''
+ );
}
/**
* Gathers the script output from the buffer, replaces some execution
* time tag in the output and displays the debug toolbar, if required.
*
- * @param mixed|null $returned
+ * @param ResponseInterface|string|null $returned
*/
protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
{
@@ -901,6 +971,11 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
}
if ($returned instanceof DownloadResponse) {
+ // Turn off output buffering completely, even if php.ini output_buffering is not off
+ while (ob_get_level() > 0) {
+ ob_end_clean();
+ }
+
$this->response = $returned;
return;
@@ -943,8 +1018,8 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
public function storePreviousURL($uri)
{
// Ignore CLI requests
- if (is_cli() && ENVIRONMENT !== 'testing') {
- return; // @codeCoverageIgnore
+ if (! $this->isWeb()) {
+ return;
}
// Ignore AJAX requests
if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
@@ -956,6 +1031,11 @@ public function storePreviousURL($uri)
return;
}
+ // Ignore non-HTML responses
+ if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) {
+ return;
+ }
+
// This is mainly needed during testing...
if (is_string($uri)) {
$uri = new URI($uri);
@@ -973,7 +1053,7 @@ public function storePreviousURL($uri)
public function spoofRequestMethod()
{
// Only works with POSTED forms
- if ($this->request->getMethod() !== 'post') {
+ if (strtolower($this->request->getMethod()) !== 'post') {
return;
}
@@ -1011,4 +1091,18 @@ protected function callExit($code)
{
exit($code); // @codeCoverageIgnore
}
+
+ /**
+ * Sets the app context.
+ *
+ * @phpstan-param 'php-cli'|'spark'|'web' $context
+ *
+ * @return $this
+ */
+ public function setContext(string $context)
+ {
+ $this->context = $context;
+
+ return $this;
+ }
}
diff --git a/system/Commands/Database/Migrate.php b/system/Commands/Database/Migrate.php
index 91a699b346d6..bcf71cade337 100644
--- a/system/Commands/Database/Migrate.php
+++ b/system/Commands/Database/Migrate.php
@@ -91,7 +91,7 @@ public function run(array $params)
CLI::write($message);
}
- CLI::write('Done migrations.', 'green');
+ CLI::write(lang('Migrations.migrated'), 'green');
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php
index 91b52950b2b4..8b0885a659b1 100644
--- a/system/Commands/Database/MigrateStatus.php
+++ b/system/Commands/Database/MigrateStatus.php
@@ -79,8 +79,8 @@ class MigrateStatus extends BaseCommand
*/
public function run(array $params)
{
- $runner = Services::migrations();
- $group = $params['g'] ?? CLI::getOption('g');
+ $runner = Services::migrations();
+ $paramGroup = $params['g'] ?? CLI::getOption('g');
// Get all namespaces
$namespaces = Services::autoloader()->getNamespace();
@@ -108,7 +108,8 @@ public function run(array $params)
continue;
}
- $history = $runner->getHistory((string) $group);
+ $runner->setNamespace($namespace);
+ $history = $runner->getHistory((string) $paramGroup);
ksort($migrations);
foreach ($migrations as $uid => $migration) {
diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php
new file mode 100644
index 000000000000..7e3d6a4807ff
--- /dev/null
+++ b/system/Commands/Database/ShowTableInfo.php
@@ -0,0 +1,284 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Database;
+
+use CodeIgniter\CLI\BaseCommand;
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\Database\BaseConnection;
+use Config\Database;
+
+/**
+ * Get table data if it exists in the database.
+ */
+class ShowTableInfo extends BaseCommand
+{
+ /**
+ * The group the command is lumped under
+ * when listing commands.
+ *
+ * @var string
+ */
+ protected $group = 'Database';
+
+ /**
+ * The Command's name
+ *
+ * @var string
+ */
+ protected $name = 'db:table';
+
+ /**
+ * the Command's short description
+ *
+ * @var string
+ */
+ protected $description = 'Retrieves information on the selected table.';
+
+ /**
+ * the Command's usage
+ *
+ * @var string
+ */
+ protected $usage = <<<'EOL'
+ db:table [] [options]
+
+ Examples:
+ db:table --show
+ db:table --metadata
+ db:table my_table --metadata
+ db:table my_table
+ db:table my_table --limit-rows 5 --limit-field-value 10 --desc
+ EOL;
+
+ /**
+ * The Command's arguments
+ *
+ * @var array
+ */
+ protected $arguments = [
+ 'table_name' => 'The table name to show info',
+ ];
+
+ /**
+ * The Command's options
+ *
+ * @var array
+ */
+ protected $options = [
+ '--show' => 'Lists the names of all database tables.',
+ '--metadata' => 'Retrieves list containing field information.',
+ '--desc' => '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.',
+ ];
+
+ /**
+ * @phpstan-var list> Table Data.
+ */
+ private array $tbody;
+
+ private BaseConnection $db;
+
+ /**
+ * @var bool Sort the table rows in DESC order or not.
+ */
+ private bool $sortDesc = false;
+
+ private string $DBPrefix;
+
+ public function run(array $params)
+ {
+ $this->db = Database::connect();
+ $this->DBPrefix = $this->db->getPrefix();
+
+ $tables = $this->db->listTables();
+
+ if (array_key_exists('desc', $params)) {
+ $this->sortDesc = true;
+ }
+
+ if ($tables === []) {
+ CLI::error('Database has no tables!', 'light_gray', 'red');
+ CLI::newLine();
+
+ return;
+ }
+
+ if (array_key_exists('show', $params)) {
+ $this->showAllTables($tables);
+
+ return;
+ }
+
+ $tableName = $params[0] ?? null;
+ $limitRows = (int) ($params['limit-rows'] ?? 10);
+ $limitFieldValue = (int) ($params['limit-field-value'] ?? 15);
+
+ if (! in_array($tableName, $tables, true)) {
+ $tableNameNo = CLI::promptByKey(
+ ['Here is the list of your database tables:', 'Which table do you want to see?'],
+ $tables,
+ 'required'
+ );
+
+ $tableName = $tables[$tableNameNo];
+ }
+
+ if (array_key_exists('metadata', $params)) {
+ $this->showFieldMetaData($tableName);
+
+ return;
+ }
+
+ $this->showDataOfTable($tableName, $limitRows, $limitFieldValue);
+ }
+
+ private function removeDBPrefix(): void
+ {
+ $this->db->setPrefix('');
+ }
+
+ private function restoreDBPrefix(): void
+ {
+ $this->db->setPrefix($this->DBPrefix);
+ }
+
+ private function showDataOfTable(string $tableName, int $limitRows, int $limitFieldValue)
+ {
+ CLI::newLine();
+ CLI::write("Data of Table \"{$tableName}\":", 'black', 'yellow');
+ CLI::newLine();
+
+ $this->removeDBPrefix();
+ $thead = $this->db->getFieldNames($tableName);
+ $this->restoreDBPrefix();
+
+ // If there is a field named `id`, sort by it.
+ $sortField = null;
+ if (in_array('id', $thead, true)) {
+ $sortField = 'id';
+ }
+
+ $this->tbody = $this->makeTableRows($tableName, $limitRows, $limitFieldValue, $sortField);
+ CLI::table($this->tbody, $thead);
+ }
+
+ private function showAllTables(array $tables)
+ {
+ CLI::write('The following is a list of the names of all database tables:', 'black', 'yellow');
+ CLI::newLine();
+
+ $thead = ['ID', 'Table Name', 'Num of Rows', 'Num of Fields'];
+ $this->tbody = $this->makeTbodyForShowAllTables($tables);
+
+ CLI::table($this->tbody, $thead);
+ CLI::newLine();
+ }
+
+ private function makeTbodyForShowAllTables(array $tables): array
+ {
+ $this->removeDBPrefix();
+
+ foreach ($tables as $id => $tableName) {
+ $table = $this->db->protectIdentifiers($tableName);
+ $db = $this->db->query("SELECT * FROM {$table}");
+
+ $this->tbody[] = [
+ $id + 1,
+ $tableName,
+ $db->getNumRows(),
+ $db->getFieldCount(),
+ ];
+ }
+
+ $this->restoreDBPrefix();
+
+ if ($this->sortDesc) {
+ krsort($this->tbody);
+ }
+
+ return $this->tbody;
+ }
+
+ private function makeTableRows(
+ string $tableName,
+ int $limitRows,
+ int $limitFieldValue,
+ ?string $sortField = null
+ ): array {
+ $this->tbody = [];
+
+ $this->removeDBPrefix();
+ $builder = $this->db->table($tableName);
+ $builder->limit($limitRows);
+ if ($sortField !== null) {
+ $builder->orderBy($sortField, $this->sortDesc ? 'DESC' : 'ASC');
+ }
+ $rows = $builder->get()->getResultArray();
+ $this->restoreDBPrefix();
+
+ foreach ($rows as $row) {
+ $row = array_map(
+ static fn ($item): string => mb_strlen((string) $item) > $limitFieldValue
+ ? mb_substr((string) $item, 0, $limitFieldValue) . '...'
+ : (string) $item,
+ $row
+ );
+ $this->tbody[] = $row;
+ }
+
+ if ($sortField === null && $this->sortDesc) {
+ krsort($this->tbody);
+ }
+
+ return $this->tbody;
+ }
+
+ private function showFieldMetaData(string $tableName): void
+ {
+ CLI::newLine();
+ CLI::write("List of Metadata Information in Table \"{$tableName}\":", 'black', 'yellow');
+ CLI::newLine();
+
+ $thead = ['Field Name', 'Type', 'Max Length', 'Nullable', 'Default', 'Primary Key'];
+
+ $this->removeDBPrefix();
+ $fields = $this->db->getFieldData($tableName);
+ $this->restoreDBPrefix();
+
+ foreach ($fields as $row) {
+ $this->tbody[] = [
+ $row->name,
+ $row->type,
+ $row->max_length,
+ isset($row->nullable) ? $this->setYesOrNo($row->nullable) : 'n/a',
+ $row->default,
+ isset($row->primary_key) ? $this->setYesOrNo($row->primary_key) : 'n/a',
+ ];
+ }
+
+ if ($this->sortDesc) {
+ krsort($this->tbody);
+ }
+
+ CLI::table($this->tbody, $thead);
+ }
+
+ private function setYesOrNo(bool $fieldValue): string
+ {
+ if ($fieldValue) {
+ return CLI::color('Yes', 'green');
+ }
+
+ return CLI::color('No', 'red');
+ }
+}
diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php
index 810f2dc92d39..b9d1794ff1fb 100644
--- a/system/Commands/Encryption/GenerateKey.php
+++ b/system/Commands/Encryption/GenerateKey.php
@@ -151,8 +151,8 @@ protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey):
$baseEnv = ROOTPATH . 'env';
$envFile = ROOTPATH . '.env';
- if (! file_exists($envFile)) {
- if (! file_exists($baseEnv)) {
+ if (! is_file($envFile)) {
+ if (! is_file($baseEnv)) {
CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow');
CLI::write('Here\'s your new key instead: ' . CLI::color($newKey, 'yellow'));
CLI::newLine();
diff --git a/system/Commands/Generators/ControllerGenerator.php b/system/Commands/Generators/ControllerGenerator.php
index 36a951cf0df0..f27c77ee0fe5 100644
--- a/system/Commands/Generators/ControllerGenerator.php
+++ b/system/Commands/Generators/ControllerGenerator.php
@@ -14,6 +14,9 @@
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
+use CodeIgniter\Controller;
+use CodeIgniter\RESTful\ResourceController;
+use CodeIgniter\RESTful\ResourcePresenter;
/**
* Generates a skeleton controller file.
@@ -99,7 +102,7 @@ protected function prepare(string $class): string
// Gets the appropriate parent class to extend.
if ($bare || $rest) {
if ($bare) {
- $useStatement = 'CodeIgniter\Controller';
+ $useStatement = Controller::class;
$extends = 'Controller';
} elseif ($rest) {
$rest = is_string($rest) ? $rest : 'controller';
@@ -112,10 +115,10 @@ protected function prepare(string $class): string
}
if ($rest === 'controller') {
- $useStatement = 'CodeIgniter\RESTful\ResourceController';
+ $useStatement = ResourceController::class;
$extends = 'ResourceController';
} elseif ($rest === 'presenter') {
- $useStatement = 'CodeIgniter\RESTful\ResourcePresenter';
+ $useStatement = ResourcePresenter::class;
$extends = 'ResourcePresenter';
}
}
diff --git a/system/Commands/Generators/MigrateCreate.php b/system/Commands/Generators/MigrateCreate.php
index 4803dc96db25..0b4935342831 100644
--- a/system/Commands/Generators/MigrateCreate.php
+++ b/system/Commands/Generators/MigrateCreate.php
@@ -77,9 +77,9 @@ class MigrateCreate extends BaseCommand
public function run(array $params)
{
// Resolve arguments before passing to make:migration
- $params[0] = $params[0] ?? CLI::getSegment(2);
+ $params[0] ??= CLI::getSegment(2);
- $params['namespace'] = $params['namespace'] ?? CLI::getOption('namespace') ?? APP_NAMESPACE;
+ $params['namespace'] ??= CLI::getOption('namespace') ?? APP_NAMESPACE;
if (array_key_exists('force', $params) || CLI::getOption('force')) {
$params['force'] = null;
diff --git a/system/Commands/Help.php b/system/Commands/Help.php
index 7562333a4f09..4dbc2df6d34d 100644
--- a/system/Commands/Help.php
+++ b/system/Commands/Help.php
@@ -71,8 +71,8 @@ class Help extends BaseCommand
*/
public function run(array $params)
{
- $command = array_shift($params);
- $command = $command ?? 'help';
+ $command = array_shift($params);
+ $command ??= 'help';
$commands = $this->commands->getCommands();
if (! $this->commands->verifyCommand($command, $commands)) {
diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php
index 5586de575dbd..b844e7a260f2 100644
--- a/system/Commands/Utilities/Environment.php
+++ b/system/Commands/Utilities/Environment.php
@@ -72,7 +72,7 @@ final class Environment extends BaseCommand
*
* @var array
*/
- private static $knownTypes = [
+ private static array $knownTypes = [
'production',
'development',
];
diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php
index 210d1e11c5f7..c8fc46bf3876 100644
--- a/system/Commands/Utilities/Routes.php
+++ b/system/Commands/Utilities/Routes.php
@@ -11,14 +11,19 @@
namespace CodeIgniter\Commands\Utilities;
+use Closure;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
+use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
+use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved;
+use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
+use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator;
use Config\Services;
/**
- * Lists all of the user-defined routes. This will include any Routes files
- * that can be discovered, but will NOT include any routes that are not defined
- * in a routes file, but are instead discovered through auto-routing.
+ * Lists all the routes. This will include any Routes files
+ * that can be discovered, and will include routes that are not defined
+ * in routes files, but are instead discovered through auto-routing.
*/
class Routes extends BaseCommand
{
@@ -42,7 +47,7 @@ class Routes extends BaseCommand
*
* @var string
*/
- protected $description = 'Displays all of user-defined routes. Does NOT display auto-detected routes.';
+ protected $description = 'Displays all routes.';
/**
* the Command's usage
@@ -84,27 +89,69 @@ public function run(array $params)
'cli',
];
- $tbody = [];
+ $tbody = [];
+ $uriGenerator = new SampleURIGenerator();
+ $filterCollector = new FilterCollector();
foreach ($methods as $method) {
$routes = $collection->getRoutes($method);
foreach ($routes as $route => $handler) {
- // filter for strings, as callbacks aren't displayable
- if (is_string($handler)) {
+ if (is_string($handler) || $handler instanceof Closure) {
+ $sampleUri = $uriGenerator->get($route);
+ $filters = $filterCollector->get($method, $sampleUri);
+
$tbody[] = [
strtoupper($method),
$route,
- $handler,
+ is_string($handler) ? $handler : '(Closure)',
+ implode(' ', array_map('class_basename', $filters['before'])),
+ implode(' ', array_map('class_basename', $filters['after'])),
];
}
}
}
+ if ($collection->shouldAutoRoute()) {
+ $autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false;
+
+ if ($autoRoutesImproved) {
+ $autoRouteCollector = new AutoRouteCollectorImproved(
+ $collection->getDefaultNamespace(),
+ $collection->getDefaultController(),
+ $collection->getDefaultMethod(),
+ $methods,
+ $collection->getRegisteredControllers('*')
+ );
+
+ $autoRoutes = $autoRouteCollector->get();
+ } else {
+ $autoRouteCollector = new AutoRouteCollector(
+ $collection->getDefaultNamespace(),
+ $collection->getDefaultController(),
+ $collection->getDefaultMethod()
+ );
+
+ $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]));
+
+ $routes[] = implode(' ', array_map('class_basename', $filters['before']));
+ $routes[] = implode(' ', array_map('class_basename', $filters['after']));
+ }
+ }
+
+ $tbody = [...$tbody, ...$autoRoutes];
+ }
+
$thead = [
'Method',
'Route',
'Handler',
+ 'Before Filters',
+ 'After Filters',
];
CLI::table($tbody, $thead);
diff --git a/system/Commands/Utilities/Routes/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouteCollector.php
new file mode 100644
index 000000000000..30c8eecfee9d
--- /dev/null
+++ b/system/Commands/Utilities/Routes/AutoRouteCollector.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+/**
+ * Collects data for auto route listing.
+ */
+final class AutoRouteCollector
+{
+ /**
+ * @var string namespace to search
+ */
+ private string $namespace;
+
+ private string $defaultController;
+ private string $defaultMethod;
+
+ /**
+ * @param string $namespace namespace to search
+ */
+ public function __construct(string $namespace, string $defaultController, string $defaultMethod)
+ {
+ $this->namespace = $namespace;
+ $this->defaultController = $defaultController;
+ $this->defaultMethod = $defaultMethod;
+ }
+
+ /**
+ * @return array>
+ * @phpstan-return list>
+ */
+ public function get(): array
+ {
+ $finder = new ControllerFinder($this->namespace);
+ $reader = new ControllerMethodReader($this->namespace);
+
+ $tbody = [];
+
+ foreach ($finder->find() as $class) {
+ $output = $reader->read(
+ $class,
+ $this->defaultController,
+ $this->defaultMethod
+ );
+
+ foreach ($output as $item) {
+ $tbody[] = [
+ 'auto',
+ $item['route'],
+ $item['handler'],
+ ];
+ }
+ }
+
+ return $tbody;
+ }
+}
diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
new file mode 100644
index 000000000000..0df2e6bff490
--- /dev/null
+++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
@@ -0,0 +1,138 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
+
+use CodeIgniter\Commands\Utilities\Routes\ControllerFinder;
+use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
+
+/**
+ * Collects data for Auto Routing Improved.
+ */
+final class AutoRouteCollector
+{
+ /**
+ * @var string namespace to search
+ */
+ private string $namespace;
+
+ private string $defaultController;
+ private string $defaultMethod;
+ private array $httpMethods;
+
+ /**
+ * List of controllers in Defined Routes that should not be accessed via Auto-Routing.
+ *
+ * @var class-string[]
+ */
+ private array $protectedControllers;
+
+ /**
+ * @param string $namespace namespace to search
+ */
+ public function __construct(
+ string $namespace,
+ string $defaultController,
+ string $defaultMethod,
+ array $httpMethods,
+ array $protectedControllers
+ ) {
+ $this->namespace = $namespace;
+ $this->defaultController = $defaultController;
+ $this->defaultMethod = $defaultMethod;
+ $this->httpMethods = $httpMethods;
+ $this->protectedControllers = $protectedControllers;
+ }
+
+ /**
+ * @return array>
+ * @phpstan-return list>
+ */
+ public function get(): array
+ {
+ $finder = new ControllerFinder($this->namespace);
+ $reader = new ControllerMethodReader($this->namespace, $this->httpMethods);
+
+ $tbody = [];
+
+ foreach ($finder->find() as $class) {
+ // Exclude controllers in Defined Routes.
+ if (in_array($class, $this->protectedControllers, true)) {
+ continue;
+ }
+
+ $routes = $reader->read(
+ $class,
+ $this->defaultController,
+ $this->defaultMethod
+ );
+
+ if ($routes === []) {
+ continue;
+ }
+
+ $routes = $this->addFilters($routes);
+
+ foreach ($routes as $item) {
+ $tbody[] = [
+ strtoupper($item['method']) . '(auto)',
+ $item['route'] . $item['route_params'],
+ $item['handler'],
+ $item['before'],
+ $item['after'],
+ ];
+ }
+ }
+
+ return $tbody;
+ }
+
+ private function addFilters($routes)
+ {
+ $filterCollector = new FilterCollector(true);
+
+ foreach ($routes as &$route) {
+ // Search filters for the URI with all params
+ $sampleUri = $this->generateSampleUri($route);
+ $filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);
+
+ // Search filters for the URI without optional params
+ $sampleUri = $this->generateSampleUri($route, false);
+ $filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);
+
+ // Get common array elements
+ $filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']);
+ $filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']);
+
+ $route['before'] = implode(' ', array_map('class_basename', $filters['before']));
+ $route['after'] = implode(' ', array_map('class_basename', $filters['after']));
+ }
+
+ return $routes;
+ }
+
+ private function generateSampleUri(array $route, bool $longest = true): string
+ {
+ $sampleUri = '';
+
+ if (isset($route['params'])) {
+ $i = 1;
+
+ foreach ($route['params'] as $required) {
+ if ($longest && ! $required) {
+ $sampleUri .= '/' . $i++;
+ }
+ }
+ }
+
+ return $sampleUri;
+ }
+}
diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
new file mode 100644
index 000000000000..3f373c433f1b
--- /dev/null
+++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
@@ -0,0 +1,182 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
+
+use ReflectionClass;
+use ReflectionMethod;
+
+/**
+ * Reads a controller and returns a list of auto route listing.
+ */
+final class ControllerMethodReader
+{
+ /**
+ * @var string the default namespace
+ */
+ private string $namespace;
+
+ private array $httpMethods;
+
+ /**
+ * @param string $namespace the default namespace
+ */
+ public function __construct(string $namespace, array $httpMethods)
+ {
+ $this->namespace = $namespace;
+ $this->httpMethods = $httpMethods;
+ }
+
+ /**
+ * Returns found route info in the controller.
+ *
+ * @phpstan-param class-string $class
+ *
+ * @return array>
+ * @phpstan-return list>
+ */
+ public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
+ {
+ $reflection = new ReflectionClass($class);
+
+ if ($reflection->isAbstract()) {
+ return [];
+ }
+
+ $classname = $reflection->getName();
+ $classShortname = $reflection->getShortName();
+
+ $output = [];
+ $classInUri = $this->getUriByClass($classname);
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $methodName = $method->getName();
+
+ foreach ($this->httpMethods as $httpVerb) {
+ if (strpos($methodName, $httpVerb) === 0) {
+ // Remove HTTP verb prefix.
+ $methodInUri = lcfirst(substr($methodName, strlen($httpVerb)));
+
+ if ($methodInUri === $defaultMethod) {
+ $routeWithoutController = $this->getRouteWithoutController(
+ $classShortname,
+ $defaultController,
+ $classInUri,
+ $classname,
+ $methodName,
+ $httpVerb
+ );
+
+ if ($routeWithoutController !== []) {
+ $output = [...$output, ...$routeWithoutController];
+
+ continue;
+ }
+
+ // Route for the default method.
+ $output[] = [
+ 'method' => $httpVerb,
+ 'route' => $classInUri,
+ 'route_params' => '',
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ 'params' => [],
+ ];
+
+ continue;
+ }
+
+ $route = $classInUri . '/' . $methodInUri;
+
+ $params = [];
+ $routeParams = '';
+ $refParams = $method->getParameters();
+
+ foreach ($refParams as $param) {
+ $required = true;
+ if ($param->isOptional()) {
+ $required = false;
+
+ $routeParams .= '[/..]';
+ } else {
+ $routeParams .= '/..';
+ }
+
+ // [variable_name => required?]
+ $params[$param->getName()] = $required;
+ }
+
+ $output[] = [
+ 'method' => $httpVerb,
+ 'route' => $route,
+ 'route_params' => $routeParams,
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ 'params' => $params,
+ ];
+ }
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * @phpstan-param class-string $classname
+ *
+ * @return string URI path part from the folder(s) and controller
+ */
+ private function getUriByClass(string $classname): string
+ {
+ // remove the namespace
+ $pattern = '/' . preg_quote($this->namespace, '/') . '/';
+ $class = ltrim(preg_replace($pattern, '', $classname), '\\');
+
+ $classParts = explode('\\', $class);
+ $classPath = '';
+
+ foreach ($classParts as $part) {
+ // make the first letter lowercase, because auto routing makes
+ // the URI path's first letter uppercase and search the controller
+ $classPath .= lcfirst($part) . '/';
+ }
+
+ return rtrim($classPath, '/');
+ }
+
+ /**
+ * Gets a route without default controller.
+ */
+ private function getRouteWithoutController(
+ string $classShortname,
+ string $defaultController,
+ string $uriByClass,
+ string $classname,
+ string $methodName,
+ string $httpVerb
+ ): array {
+ $output = [];
+
+ if ($classShortname === $defaultController) {
+ $pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
+ $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
+ $routeWithoutController = $routeWithoutController ?: '/';
+
+ $output[] = [
+ 'method' => $httpVerb,
+ 'route' => $routeWithoutController,
+ 'route_params' => '',
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ 'params' => [],
+ ];
+ }
+
+ return $output;
+ }
+}
diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php
new file mode 100644
index 000000000000..7e7865876750
--- /dev/null
+++ b/system/Commands/Utilities/Routes/ControllerFinder.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+use CodeIgniter\Autoloader\FileLocator;
+use CodeIgniter\Config\Services;
+
+/**
+ * Finds all controllers in a namespace for auto route listing.
+ */
+final class ControllerFinder
+{
+ /**
+ * @var string namespace to search
+ */
+ private string $namespace;
+
+ private FileLocator $locator;
+
+ /**
+ * @param string $namespace namespace to search
+ */
+ public function __construct(string $namespace)
+ {
+ $this->namespace = $namespace;
+ $this->locator = Services::locator();
+ }
+
+ /**
+ * @return string[]
+ * @phpstan-return class-string[]
+ */
+ public function find(): array
+ {
+ $nsArray = explode('\\', trim($this->namespace, '\\'));
+ $count = count($nsArray);
+ $ns = '';
+
+ for ($i = 0; $i < $count; $i++) {
+ $ns .= '\\' . array_shift($nsArray);
+ $path = implode('\\', $nsArray);
+
+ $files = $this->locator->listNamespaceFiles($ns, $path);
+
+ if ($files !== []) {
+ break;
+ }
+ }
+
+ $classes = [];
+
+ foreach ($files as $file) {
+ if (\is_file($file)) {
+ $classnameOrEmpty = $this->locator->getClassname($file);
+
+ if ($classnameOrEmpty !== '') {
+ /** @phpstan-var class-string $classname */
+ $classname = $classnameOrEmpty;
+
+ $classes[] = $classname;
+ }
+ }
+ }
+
+ return $classes;
+ }
+}
diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php
new file mode 100644
index 000000000000..27bdad046a99
--- /dev/null
+++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php
@@ -0,0 +1,176 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+use ReflectionClass;
+use ReflectionMethod;
+
+/**
+ * Reads a controller and returns a list of auto route listing.
+ */
+final class ControllerMethodReader
+{
+ /**
+ * @var string the default namespace
+ */
+ private string $namespace;
+
+ /**
+ * @param string $namespace the default namespace
+ */
+ public function __construct(string $namespace)
+ {
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @phpstan-param class-string $class
+ *
+ * @return array
+ * @phpstan-return list
+ */
+ public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
+ {
+ $reflection = new ReflectionClass($class);
+
+ if ($reflection->isAbstract()) {
+ return [];
+ }
+
+ $classname = $reflection->getName();
+ $classShortname = $reflection->getShortName();
+
+ $output = [];
+ $uriByClass = $this->getUriByClass($classname);
+
+ if ($this->hasRemap($reflection)) {
+ $methodName = '_remap';
+
+ $routeWithoutController = $this->getRouteWithoutController(
+ $classShortname,
+ $defaultController,
+ $uriByClass,
+ $classname,
+ $methodName
+ );
+ $output = [...$output, ...$routeWithoutController];
+
+ $output[] = [
+ 'route' => $uriByClass . '[/...]',
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ ];
+
+ return $output;
+ }
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $methodName = $method->getName();
+
+ $route = $uriByClass . '/' . $methodName;
+
+ // Exclude BaseController and initController
+ // See system/Config/Routes.php
+ if (preg_match('#\AbaseController.*#', $route) === 1) {
+ continue;
+ }
+ if (preg_match('#.*/initController\z#', $route) === 1) {
+ continue;
+ }
+
+ if ($methodName === $defaultMethod) {
+ $routeWithoutController = $this->getRouteWithoutController(
+ $classShortname,
+ $defaultController,
+ $uriByClass,
+ $classname,
+ $methodName
+ );
+ $output = [...$output, ...$routeWithoutController];
+
+ $output[] = [
+ 'route' => $uriByClass,
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ ];
+ }
+
+ $output[] = [
+ 'route' => $route . '[/...]',
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ ];
+ }
+
+ return $output;
+ }
+
+ /**
+ * Whether the class has a _remap() method.
+ */
+ private function hasRemap(ReflectionClass $class): bool
+ {
+ if ($class->hasMethod('_remap')) {
+ $remap = $class->getMethod('_remap');
+
+ return $remap->isPublic();
+ }
+
+ return false;
+ }
+
+ /**
+ * @phpstan-param class-string $classname
+ *
+ * @return string URI path part from the folder(s) and controller
+ */
+ private function getUriByClass(string $classname): string
+ {
+ // remove the namespace
+ $pattern = '/' . preg_quote($this->namespace, '/') . '/';
+ $class = ltrim(preg_replace($pattern, '', $classname), '\\');
+
+ $classParts = explode('\\', $class);
+ $classPath = '';
+
+ foreach ($classParts as $part) {
+ // make the first letter lowercase, because auto routing makes
+ // the URI path's first letter uppercase and search the controller
+ $classPath .= lcfirst($part) . '/';
+ }
+
+ return rtrim($classPath, '/');
+ }
+
+ /**
+ * Gets a route without default controller.
+ */
+ private function getRouteWithoutController(
+ string $classShortname,
+ string $defaultController,
+ string $uriByClass,
+ string $classname,
+ string $methodName
+ ): array {
+ $output = [];
+
+ if ($classShortname === $defaultController) {
+ $pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
+ $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
+ $routeWithoutController = $routeWithoutController ?: '/';
+
+ $output[] = [
+ 'route' => $routeWithoutController,
+ 'handler' => '\\' . $classname . '::' . $methodName,
+ ];
+ }
+
+ return $output;
+ }
+}
diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php
new file mode 100644
index 000000000000..630416749115
--- /dev/null
+++ b/system/Commands/Utilities/Routes/FilterCollector.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+use CodeIgniter\Config\Services;
+use CodeIgniter\Filters\Filters;
+use CodeIgniter\HTTP\Request;
+use CodeIgniter\Router\Router;
+
+/**
+ * Collects filters for a route.
+ */
+final class FilterCollector
+{
+ /**
+ * Whether to reset Defined Routes.
+ *
+ * If set to true, route filters are not found.
+ */
+ private bool $resetRoutes;
+
+ public function __construct(bool $resetRoutes = false)
+ {
+ $this->resetRoutes = $resetRoutes;
+ }
+
+ /**
+ * @param string $method HTTP method
+ * @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') {
+ return [
+ 'before' => [],
+ 'after' => [],
+ ];
+ }
+
+ $request = Services::request(null, false);
+ $request->setMethod($method);
+
+ $router = $this->createRouter($request);
+ $filters = $this->createFilters($request);
+
+ $finder = new FilterFinder($router, $filters);
+
+ return $finder->find($uri);
+ }
+
+ private function createRouter(Request $request): Router
+ {
+ $routes = Services::routes();
+
+ if ($this->resetRoutes) {
+ $routes->resetRoutes();
+ }
+
+ return new Router($routes, $request);
+ }
+
+ private function createFilters(Request $request): Filters
+ {
+ $config = config('Filters');
+
+ return new Filters($config, $request, Services::response());
+ }
+}
diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php
new file mode 100644
index 000000000000..f36ddd9e362d
--- /dev/null
+++ b/system/Commands/Utilities/Routes/FilterFinder.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+use CodeIgniter\Filters\Filters;
+use CodeIgniter\Router\Exceptions\RedirectException;
+use CodeIgniter\Router\Router;
+use Config\Services;
+
+/**
+ * Finds filters.
+ */
+final class FilterFinder
+{
+ private Router $router;
+ private Filters $filters;
+
+ public function __construct(?Router $router = null, ?Filters $filters = null)
+ {
+ $this->router = $router ?? Services::router();
+ $this->filters = $filters ?? Services::filters();
+ }
+
+ private function getRouteFilters(string $uri): array
+ {
+ $this->router->handle($uri);
+
+ $multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
+ if (! $multipleFiltersEnabled) {
+ $filter = $this->router->getFilter();
+
+ return $filter === null ? [] : [$filter];
+ }
+
+ return $this->router->getFilters();
+ }
+
+ /**
+ * @param string $uri URI path to find filters for
+ *
+ * @return array{before: list, after: list} array of filter alias or classname
+ */
+ public function find(string $uri): array
+ {
+ $this->filters->reset();
+
+ // Add route filters
+ try {
+ $routeFilters = $this->getRouteFilters($uri);
+ $this->filters->enableFilters($routeFilters, 'before');
+ $this->filters->enableFilters($routeFilters, 'after');
+
+ $this->filters->initialize($uri);
+
+ return $this->filters->getFilters();
+ } catch (RedirectException $e) {
+ return [
+ 'before' => [],
+ 'after' => [],
+ ];
+ }
+ }
+}
diff --git a/system/Commands/Utilities/Routes/SampleURIGenerator.php b/system/Commands/Utilities/Routes/SampleURIGenerator.php
new file mode 100644
index 000000000000..984e50abd0e2
--- /dev/null
+++ b/system/Commands/Utilities/Routes/SampleURIGenerator.php
@@ -0,0 +1,61 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Commands\Utilities\Routes;
+
+use CodeIgniter\Config\Services;
+use CodeIgniter\Router\RouteCollection;
+
+/**
+ * Generate a sample URI path from route key regex.
+ */
+final class SampleURIGenerator
+{
+ private RouteCollection $routes;
+
+ /**
+ * Sample URI path for placeholder.
+ *
+ * @var array
+ */
+ private array $samples = [
+ 'any' => '123/abc',
+ 'segment' => 'abc_123',
+ 'alphanum' => 'abc123',
+ 'num' => '123',
+ 'alpha' => 'abc',
+ 'hash' => 'abc_123',
+ ];
+
+ public function __construct(?RouteCollection $routes = null)
+ {
+ $this->routes = $routes ?? Services::routes();
+ }
+
+ /**
+ * @param string $routeKey route key regex
+ *
+ * @return string sample URI path
+ */
+ public function get(string $routeKey): string
+ {
+ $sampleUri = $routeKey;
+
+ foreach ($this->routes->getPlaceholders() as $placeholder => $regex) {
+ $sample = $this->samples[$placeholder] ?? '::unknown::';
+
+ $sampleUri = str_replace('(' . $regex . ')', $sample, $sampleUri);
+ }
+
+ // auto route
+ return str_replace('[/...]', '/1/2/3/4/5', $sampleUri);
+ }
+}
diff --git a/system/Common.php b/system/Common.php
index e132d982b7d4..30e28a1dc840 100644
--- a/system/Common.php
+++ b/system/Common.php
@@ -288,6 +288,38 @@ function csrf_meta(?string $id = null): string
}
}
+if (! function_exists('csp_style_nonce')) {
+ /**
+ * Generates a nonce attribute for style tag.
+ */
+ function csp_style_nonce(): string
+ {
+ $csp = Services::csp();
+
+ if (! $csp->enabled()) {
+ return '';
+ }
+
+ return 'nonce="' . $csp->getStyleNonce() . '"';
+ }
+}
+
+if (! function_exists('csp_script_nonce')) {
+ /**
+ * Generates a nonce attribute for script tag.
+ */
+ function csp_script_nonce(): string
+ {
+ $csp = Services::csp();
+
+ if (! $csp->enabled()) {
+ return '';
+ }
+
+ return 'nonce="' . $csp->getScriptNonce() . '"';
+ }
+}
+
if (! function_exists('db_connect')) {
/**
* Grabs a database connection and returns it to the user.
@@ -445,7 +477,7 @@ function esc($data, string $context = 'html', ?string $encoding = null)
*
* @throws HTTPException
*/
- function force_https(int $duration = 31536000, ?RequestInterface $request = null, ?ResponseInterface $response = null)
+ function force_https(int $duration = 31_536_000, ?RequestInterface $request = null, ?ResponseInterface $response = null)
{
if ($request === null) {
$request = Services::request(null, true);
@@ -618,7 +650,7 @@ function helper($filenames)
}
// All namespaced files get added in next
- $includes = array_merge($includes, $localIncludes);
+ $includes = [...$includes, ...$localIncludes];
// And the system default one should be added in last.
if (! empty($systemHelper)) {
@@ -773,7 +805,6 @@ function log_message(string $level, string $message, array $context = [])
* @param class-string $name
*
* @return T
- * @phpstan-return Model
*/
function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null)
{
@@ -950,7 +981,6 @@ function single_service(string $name, ...$params)
$method = new ReflectionMethod($service, $name);
$count = $method->getNumberOfParameters();
$mParam = $method->getParameters();
- $params = $params ?? [];
if ($count === 1) {
// This service needs only one argument, which is the shared
@@ -983,10 +1013,26 @@ function single_service(string $name, ...$params)
*/
function slash_item(string $item): ?string
{
- $config = config(App::class);
+ $config = config(App::class);
+
+ if (! property_exists($config, $item)) {
+ return null;
+ }
+
$configItem = $config->{$item};
- if (! isset($configItem) || empty(trim($configItem))) {
+ if (! is_scalar($configItem)) {
+ throw new RuntimeException(sprintf(
+ 'Cannot convert "%s::$%s" of type "%s" to type "string".',
+ App::class,
+ $item,
+ gettype($configItem)
+ ));
+ }
+
+ $configItem = trim((string) $configItem);
+
+ if ($configItem === '') {
return $configItem;
}
@@ -1095,7 +1141,7 @@ function view(string $name, array $data = [], array $options = []): string
* View cells are used within views to insert HTML chunks that are managed
* by other classes.
*
- * @param null $params
+ * @param array|string|null $params
*
* @throws ReflectionException
*/
diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php
index ef4dfe1a801b..62e5d828e1ce 100644
--- a/system/ComposerScripts.php
+++ b/system/ComposerScripts.php
@@ -30,10 +30,8 @@ final class ComposerScripts
{
/**
* Path to the ThirdParty directory.
- *
- * @var string
*/
- private static $path = __DIR__ . '/ThirdParty/';
+ private static string $path = __DIR__ . '/ThirdParty/';
/**
* Direct dependencies of CodeIgniter to copy
@@ -41,7 +39,7 @@ final class ComposerScripts
*
* @var array>
*/
- private static $dependencies = [
+ private static array $dependencies = [
'kint-src' => [
'license' => __DIR__ . '/../vendor/kint-php/kint/LICENSE',
'from' => __DIR__ . '/../vendor/kint-php/kint/src/',
diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php
index 0818271e002f..79cad2ab8d25 100644
--- a/system/Config/AutoloadConfig.php
+++ b/system/Config/AutoloadConfig.php
@@ -11,6 +11,19 @@
namespace CodeIgniter\Config;
+use Laminas\Escaper\Escaper;
+use Laminas\Escaper\Exception\ExceptionInterface;
+use Laminas\Escaper\Exception\InvalidArgumentException as EscaperInvalidArgumentException;
+use Laminas\Escaper\Exception\RuntimeException;
+use Psr\Log\AbstractLogger;
+use Psr\Log\InvalidArgumentException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
+use Psr\Log\LogLevel;
+use Psr\Log\NullLogger;
+
/**
* AUTOLOADER CONFIGURATION
*
@@ -93,15 +106,18 @@ class AutoloadConfig
* @var array
*/
protected $coreClassmap = [
- 'Psr\Log\AbstractLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php',
- 'Psr\Log\InvalidArgumentException' => SYSTEMPATH . 'ThirdParty/PSR/Log/InvalidArgumentException.php',
- 'Psr\Log\LoggerAwareInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php',
- 'Psr\Log\LoggerAwareTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php',
- 'Psr\Log\LoggerInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerInterface.php',
- 'Psr\Log\LoggerTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerTrait.php',
- 'Psr\Log\LogLevel' => SYSTEMPATH . 'ThirdParty/PSR/Log/LogLevel.php',
- 'Psr\Log\NullLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/NullLogger.php',
- 'Laminas\Escaper\Escaper' => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php',
+ AbstractLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php',
+ InvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/PSR/Log/InvalidArgumentException.php',
+ LoggerAwareInterface::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php',
+ LoggerAwareTrait::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php',
+ LoggerInterface::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerInterface.php',
+ LoggerTrait::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerTrait.php',
+ LogLevel::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LogLevel.php',
+ NullLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/NullLogger.php',
+ ExceptionInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/ExceptionInterface.php',
+ EscaperInvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/InvalidArgumentException.php',
+ RuntimeException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/RuntimeException.php',
+ Escaper::class => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php',
];
/**
@@ -130,6 +146,6 @@ public function __construct()
$this->psr4 = array_merge($this->corePsr4, $this->psr4);
$this->classmap = array_merge($this->coreClassmap, $this->classmap);
- $this->files = array_merge($this->coreFiles, $this->files);
+ $this->files = [...$this->coreFiles, ...$this->files];
}
}
diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php
index 9b71a4963bab..359fcc1488ba 100644
--- a/system/Config/BaseConfig.php
+++ b/system/Config/BaseConfig.php
@@ -88,7 +88,7 @@ public function __construct()
*
* @param mixed $property
*
- * @return mixed
+ * @return void
*/
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix)
{
@@ -102,16 +102,28 @@ protected function initEnvValue(&$property, string $name, string $prefix, string
} elseif ($value === 'true') {
$value = true;
}
- $property = is_bool($value) ? $value : trim($value, '\'"');
- }
+ if (is_bool($value)) {
+ $property = $value;
+
+ return;
+ }
+
+ $value = trim($value, '\'"');
- return $property;
+ if (is_int($property)) {
+ $value = (int) $value;
+ } elseif (is_float($property)) {
+ $value = (float) $value;
+ }
+
+ $property = $value;
+ }
}
/**
* Retrieve an environment-specific configuration setting
*
- * @return mixed
+ * @return string|null
*/
protected function getEnvValue(string $property, string $prefix, string $shortPrefix)
{
diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php
index d390807fc51f..d77cfb1bbc51 100644
--- a/system/Config/BaseService.php
+++ b/system/Config/BaseService.php
@@ -28,6 +28,7 @@
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\ContentSecurityPolicy;
use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Negotiate;
@@ -56,6 +57,7 @@
use Config\App;
use Config\Autoload;
use Config\Cache;
+use Config\ContentSecurityPolicy as CSPConfig;
use Config\Encryption;
use Config\Exceptions as ConfigExceptions;
use Config\Filters as ConfigFilters;
@@ -94,6 +96,7 @@
* @method static CLIRequest clirequest(App $config = null, $getShared = true)
* @method static CodeIgniter codeigniter(App $config = null, $getShared = true)
* @method static Commands commands($getShared = true)
+ * @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true)
* @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true)
* @method static Email email($config = null, $getShared = true)
* @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false)
@@ -162,7 +165,7 @@ class BaseService
*
* @var array
*/
- private static $serviceNames = [];
+ private static array $serviceNames = [];
/**
* Returns a shared instance of any of the class' services.
@@ -270,7 +273,7 @@ public static function serviceExists(string $name): ?string
/**
* Reset shared instances and mocks for testing.
*/
- public static function reset(bool $initAutoloader = false)
+ public static function reset(bool $initAutoloader = true)
{
static::$mocks = [];
static::$instances = [];
@@ -285,6 +288,7 @@ public static function reset(bool $initAutoloader = false)
*/
public static function resetSingle(string $name)
{
+ $name = strtolower($name);
unset(static::$mocks[$name], static::$instances[$name]);
}
@@ -328,7 +332,7 @@ protected static function discoverServices(string $name, array $arguments)
foreach ($files as $file) {
$classname = $locator->getClassname($file);
- if (! in_array($classname, ['CodeIgniter\\Config\\Services'], true)) {
+ if (! in_array($classname, [Services::class], true)) {
static::$services[] = new $classname();
}
}
@@ -365,7 +369,7 @@ protected static function buildServicesCache(): void
foreach ($files as $file) {
$classname = $locator->getClassname($file);
- if ($classname !== 'CodeIgniter\\Config\\Services') {
+ if ($classname !== Services::class) {
self::$serviceNames[] = $classname;
static::$services[] = new $classname();
}
diff --git a/system/Config/Factories.php b/system/Config/Factories.php
index 8bcef3e09610..31188c1b03a1 100644
--- a/system/Config/Factories.php
+++ b/system/Config/Factories.php
@@ -11,6 +11,7 @@
namespace CodeIgniter\Config;
+use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
use Config\Services;
@@ -23,7 +24,6 @@
* instantiation checks.
*
* @method static BaseConfig config(...$arguments)
- * @method static Model models(...$arguments)
*/
class Factories
{
@@ -41,7 +41,7 @@ class Factories
*
* @var array
*/
- private static $configOptions = [
+ private static array $configOptions = [
'component' => 'config',
'path' => 'Config',
'instanceOf' => null,
@@ -67,6 +67,22 @@ class Factories
*/
protected static $instances = [];
+ /**
+ * This method is only to prevent PHPStan error.
+ * If we have a solution, we can remove this method.
+ * See https://github.com/codeigniter4/CodeIgniter4/pull/5358
+ *
+ * @template T of Model
+ *
+ * @param class-string $name
+ *
+ * @return T
+ */
+ public static function models(string $name, array $options = [], ?ConnectionInterface &$conn = null)
+ {
+ return self::__callStatic('models', [$name, $options, $conn]);
+ }
+
/**
* Loads instances based on the method component name. Either
* creates a new instance or returns an existing shared instance.
@@ -263,7 +279,7 @@ public static function setOptions(string $component, array $values): array
/**
* Resets the static arrays, optionally just for one component
*
- * @param string $component Lowercase, plural component name
+ * @param string|null $component Lowercase, plural component name
*/
public static function reset(?string $component = null)
{
diff --git a/system/Config/Routes.php b/system/Config/Routes.php
index 4ace952bc3c8..0f16b675a8ad 100644
--- a/system/Config/Routes.php
+++ b/system/Config/Routes.php
@@ -9,8 +9,6 @@
* the LICENSE file that was distributed with this source code.
*/
-use CodeIgniter\Exceptions\PageNotFoundException;
-
/*
* System URI Routing
*
@@ -21,20 +19,5 @@
* already loaded up and ready for us to use.
*/
-// Prevent access to BaseController
-$routes->add('BaseController(:any)', static function () {
- throw PageNotFoundException::forPageNotFound();
-});
-
-// Prevent access to initController method
-$routes->add('(:any)/initController', static function () {
- throw PageNotFoundException::forPageNotFound();
-});
-
-// Migrations
-$routes->cli('migrations/(:segment)/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1/$2');
-$routes->cli('migrations/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1');
-$routes->cli('migrations', '\CodeIgniter\Commands\MigrationsCommand::index');
-
// CLI Catchall - uses a _remap to call Commands
$routes->cli('ci(:any)', '\CodeIgniter\CLI\CommandRunner::index/$1');
diff --git a/system/Config/Services.php b/system/Config/Services.php
index 0b681f17ce17..9dca50115f51 100644
--- a/system/Config/Services.php
+++ b/system/Config/Services.php
@@ -28,6 +28,7 @@
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\ContentSecurityPolicy;
use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Negotiate;
@@ -46,6 +47,9 @@
use CodeIgniter\Router\RouteCollectionInterface;
use CodeIgniter\Router\Router;
use CodeIgniter\Security\Security;
+use CodeIgniter\Session\Handlers\Database\MySQLiHandler;
+use CodeIgniter\Session\Handlers\Database\PostgreHandler;
+use CodeIgniter\Session\Handlers\DatabaseHandler;
use CodeIgniter\Session\Session;
use CodeIgniter\Throttle\Throttler;
use CodeIgniter\Typography\Typography;
@@ -56,6 +60,8 @@
use CodeIgniter\View\View;
use Config\App;
use Config\Cache;
+use Config\ContentSecurityPolicy as CSPConfig;
+use Config\Database;
use Config\Email as EmailConfig;
use Config\Encryption as EncryptionConfig;
use Config\Exceptions as ExceptionsConfig;
@@ -101,7 +107,7 @@ public static function cache(?Cache $config = null, bool $getShared = true)
return static::getSharedInstance('cache', $config);
}
- $config = $config ?? new Cache();
+ $config ??= new Cache();
return CacheFactory::getHandler($config);
}
@@ -118,7 +124,7 @@ public static function clirequest(?App $config = null, bool $getShared = true)
return static::getSharedInstance('clirequest', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
return new CLIRequest($config);
}
@@ -134,7 +140,7 @@ public static function codeigniter(?App $config = null, bool $getShared = true)
return static::getSharedInstance('codeigniter', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
return new CodeIgniter($config);
}
@@ -153,6 +159,22 @@ public static function commands(bool $getShared = true)
return new Commands();
}
+ /**
+ * Content Security Policy
+ *
+ * @return ContentSecurityPolicy
+ */
+ public static function csp(?CSPConfig $config = null, bool $getShared = true)
+ {
+ if ($getShared) {
+ return static::getSharedInstance('csp', $config);
+ }
+
+ $config ??= config('ContentSecurityPolicy');
+
+ return new ContentSecurityPolicy($config);
+ }
+
/**
* The CURL Request class acts as a simple HTTP client for interacting
* with other servers, typically through APIs.
@@ -165,8 +187,8 @@ public static function curlrequest(array $options = [], ?ResponseInterface $resp
return static::getSharedInstance('curlrequest', $options, $response, $config);
}
- $config = $config ?? config('App');
- $response = $response ?? new Response($config);
+ $config ??= config('App');
+ $response ??= new Response($config);
return new CURLRequest(
$config,
@@ -209,7 +231,7 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared =
return static::getSharedInstance('encrypter', $config);
}
- $config = $config ?? config('Encryption');
+ $config ??= config('Encryption');
$encryption = new Encryption($config);
return $encryption->initialize($config);
@@ -234,9 +256,9 @@ public static function exceptions(
return static::getSharedInstance('exceptions', $config, $request, $response);
}
- $config = $config ?? config('Exceptions');
- $request = $request ?? AppServices::request();
- $response = $response ?? AppServices::response();
+ $config ??= config('Exceptions');
+ $request ??= AppServices::request();
+ $response ??= AppServices::response();
return new Exceptions($config, $request, $response);
}
@@ -255,7 +277,7 @@ public static function filters(?FiltersConfig $config = null, bool $getShared =
return static::getSharedInstance('filters', $config);
}
- $config = $config ?? config('Filters');
+ $config ??= config('Filters');
return new Filters($config, AppServices::request(), AppServices::response());
}
@@ -271,7 +293,7 @@ public static function format(?FormatConfig $config = null, bool $getShared = tr
return static::getSharedInstance('format', $config);
}
- $config = $config ?? config('Format');
+ $config ??= config('Format');
return new Format($config);
}
@@ -288,7 +310,7 @@ public static function honeypot(?HoneypotConfig $config = null, bool $getShared
return static::getSharedInstance('honeypot', $config);
}
- $config = $config ?? config('Honeypot');
+ $config ??= config('Honeypot');
return new Honeypot($config);
}
@@ -305,7 +327,7 @@ public static function image(?string $handler = null, ?Images $config = null, bo
return static::getSharedInstance('image', $handler, $config);
}
- $config = $config ?? config('Images');
+ $config ??= config('Images');
$handler = $handler ?: $config->defaultHandler;
$class = $config->handlers[$handler];
@@ -371,7 +393,7 @@ public static function migrations(?Migrations $config = null, ?ConnectionInterfa
return static::getSharedInstance('migrations', $config, $db);
}
- $config = $config ?? config('Migrations');
+ $config ??= config('Migrations');
return new MigrationRunner($config, $db);
}
@@ -389,7 +411,7 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh
return static::getSharedInstance('negotiator', $request);
}
- $request = $request ?? AppServices::request();
+ $request ??= AppServices::request();
return new Negotiate($request);
}
@@ -405,8 +427,8 @@ public static function pager(?PagerConfig $config = null, ?RendererInterface $vi
return static::getSharedInstance('pager', $config, $view);
}
- $config = $config ?? config('Pager');
- $view = $view ?? AppServices::renderer();
+ $config ??= config('Pager');
+ $view ??= AppServices::renderer();
return new Pager($config, $view);
}
@@ -423,7 +445,7 @@ public static function parser(?string $viewPath = null, ?ViewConfig $config = nu
}
$viewPath = $viewPath ?: config('Paths')->viewDirectory;
- $config = $config ?? config('View');
+ $config ??= config('View');
return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
}
@@ -442,7 +464,7 @@ public static function renderer(?string $viewPath = null, ?ViewConfig $config =
}
$viewPath = $viewPath ?: config('Paths')->viewDirectory;
- $config = $config ?? config('View');
+ $config ??= config('View');
return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
}
@@ -458,7 +480,7 @@ public static function request(?App $config = null, bool $getShared = true)
return static::getSharedInstance('request', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
return new IncomingRequest(
$config,
@@ -479,7 +501,7 @@ public static function response(?App $config = null, bool $getShared = true)
return static::getSharedInstance('response', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
return new Response($config);
}
@@ -495,7 +517,7 @@ public static function redirectresponse(?App $config = null, bool $getShared = t
return static::getSharedInstance('redirectresponse', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
$response = new RedirectResponse($config);
$response->setProtocolVersion(AppServices::request()->getProtocolVersion());
@@ -529,8 +551,8 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request
return static::getSharedInstance('router', $routes, $request);
}
- $routes = $routes ?? AppServices::routes();
- $request = $request ?? AppServices::request();
+ $routes ??= AppServices::routes();
+ $request ??= AppServices::request();
return new Router($routes, $request);
}
@@ -547,7 +569,7 @@ public static function security(?App $config = null, bool $getShared = true)
return static::getSharedInstance('security', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
return new Security($config);
}
@@ -563,11 +585,25 @@ public static function session(?App $config = null, bool $getShared = true)
return static::getSharedInstance('session', $config);
}
- $config = $config ?? config('App');
+ $config ??= config('App');
$logger = AppServices::logger();
$driverName = $config->sessionDriver;
- $driver = new $driverName($config, AppServices::request()->getIPAddress());
+
+ if ($driverName === DatabaseHandler::class) {
+ $DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup;
+ $db = Database::connect($DBGroup);
+
+ $driver = $db->getPlatform();
+
+ if ($driver === 'MySQLi') {
+ $driverName = MySQLiHandler::class;
+ } elseif ($driver === 'Postgre') {
+ $driverName = PostgreHandler::class;
+ }
+ }
+
+ $driver = new $driverName($config, AppServices::request()->getIPAddress());
$driver->setLogger($logger);
$session = new Session($driver, $config);
@@ -621,7 +657,7 @@ public static function toolbar(?ToolbarConfig $config = null, bool $getShared =
return static::getSharedInstance('toolbar', $config);
}
- $config = $config ?? config('Toolbar');
+ $config ??= config('Toolbar');
return new Toolbar($config);
}
@@ -653,7 +689,7 @@ public static function validation(?ValidationConfig $config = null, bool $getSha
return static::getSharedInstance('validation', $config);
}
- $config = $config ?? config('Validation');
+ $config ??= config('Validation');
return new Validation($config, AppServices::renderer());
}
diff --git a/system/Config/View.php b/system/Config/View.php
index 5a8baeac2d6a..42d40faf879e 100644
--- a/system/Config/View.php
+++ b/system/Config/View.php
@@ -11,6 +11,8 @@
namespace CodeIgniter\Config;
+use CodeIgniter\View\ViewDecoratorInterface;
+
/**
* View configuration
*/
@@ -76,6 +78,8 @@ class View extends BaseConfig
* @var array
*/
protected $corePlugins = [
+ 'csp_script_nonce' => '\CodeIgniter\View\Plugins::cspScriptNonce',
+ 'csp_style_nonce' => '\CodeIgniter\View\Plugins::cspStyleNonce',
'current_url' => '\CodeIgniter\View\Plugins::currentURL',
'previous_url' => '\CodeIgniter\View\Plugins::previousURL',
'mailto' => '\CodeIgniter\View\Plugins::mailto',
@@ -86,6 +90,17 @@ class View extends BaseConfig
'siteURL' => '\CodeIgniter\View\Plugins::siteURL',
];
+ /**
+ * View Decorators are class methods that will be run in sequence to
+ * have a chance to alter the generated output just prior to caching
+ * the results.
+ *
+ * All classes must implement CodeIgniter\View\ViewDecoratorInterface
+ *
+ * @var class-string[]
+ */
+ public array $decorators = [];
+
/**
* Merge the built-in and developer-configured filters and plugins,
* with preference to the developer ones.
diff --git a/system/Controller.php b/system/Controller.php
index b50c8bcf3470..6116160c0fcb 100644
--- a/system/Controller.php
+++ b/system/Controller.php
@@ -97,7 +97,7 @@ public function initController(RequestInterface $request, ResponseInterface $res
*
* @throws HTTPException
*/
- protected function forceHTTPS(int $duration = 31536000)
+ protected function forceHTTPS(int $duration = 31_536_000)
{
force_https($duration, $this->request, $this->response);
}
@@ -128,13 +128,37 @@ protected function loadHelpers()
}
/**
- * A shortcut to performing validation on input data. If validation
- * is not successful, a $errors property will be set on this class.
+ * A shortcut to performing validation on Request data.
*
* @param array|string $rules
* @param array $messages An array of custom error messages
*/
protected function validate($rules, array $messages = []): bool
+ {
+ $this->setValidator($rules, $messages);
+
+ return $this->validator->withRequest($this->request)->run();
+ }
+
+ /**
+ * A shortcut to performing validation on any input data.
+ *
+ * @param array $data The data to validate
+ * @param array|string $rules
+ * @param array $messages An array of custom error messages
+ * @param string|null $dbGroup The database group to use
+ */
+ protected function validateData(array $data, $rules, array $messages = [], ?string $dbGroup = null): bool
+ {
+ $this->setValidator($rules, $messages);
+
+ return $this->validator->run($data, null, $dbGroup);
+ }
+
+ /**
+ * @param array|string $rules
+ */
+ private function setValidator($rules, array $messages): void
{
$this->validator = Services::validation();
@@ -157,6 +181,6 @@ protected function validate($rules, array $messages = []): bool
$rules = $validation->{$rules};
}
- return $this->validator->withRequest($this->request)->setRules($rules, $messages)->run();
+ $this->validator->setRules($rules, $messages);
}
}
diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php
index 150d42069949..e40d0a1201d5 100644
--- a/system/Cookie/Cookie.php
+++ b/system/Cookie/Cookie.php
@@ -95,7 +95,7 @@ class Cookie implements ArrayAccess, CloneableCookieInterface
*
* @var array
*/
- private static $defaults = [
+ private static array $defaults = [
'prefix' => '',
'expires' => 0,
'path' => '/',
@@ -110,12 +110,10 @@ class Cookie implements ArrayAccess, CloneableCookieInterface
* A cookie name can be any US-ASCII characters, except control characters,
* spaces, tabs, or separator characters.
*
- * @var string
- *
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
* @see https://tools.ietf.org/html/rfc2616#section-2.2
*/
- private static $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}";
+ private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}";
/**
* Set the default attributes to a Cookie instance by injecting
@@ -491,7 +489,7 @@ public function withPath(?string $path)
*/
public function withDomain(?string $domain)
{
- $domain = $domain ?? self::$defaults['domain'];
+ $domain ??= self::$defaults['domain'];
$this->validatePrefix($this->prefix, $this->secure, $this->path, $domain);
$cookie = clone $this;
diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
index 2a431accd2f5..ca983691ea0c 100644
--- a/system/Database/BaseBuilder.php
+++ b/system/Database/BaseBuilder.php
@@ -109,6 +109,13 @@ class BaseBuilder
*/
public $QBOrderBy = [];
+ /**
+ * QB UNION data
+ *
+ * @var array
+ */
+ protected array $QBUnion = [];
+
/**
* QB NO ESCAPE data
*
@@ -270,7 +277,7 @@ class BaseBuilder
*
* @throws DatabaseException
*/
- public function __construct($tableName, ConnectionInterface &$db, ?array $options = null)
+ public function __construct($tableName, ConnectionInterface $db, ?array $options = null)
{
if (empty($tableName)) {
throw new DatabaseException('A table must be specified when creating a new Query Builder.');
@@ -356,7 +363,7 @@ public function ignore(bool $ignore = true)
/**
* Generates the SELECT portion of the query
*
- * @param array|string $select
+ * @param array|RawSql|string $select
*
* @return $this
*/
@@ -371,6 +378,12 @@ public function select($select = '*', ?bool $escape = null)
$escape = $this->db->protectIdentifiers;
}
+ if ($select instanceof RawSql) {
+ $this->QBSelect[] = $select;
+
+ return $this;
+ }
+
foreach ($select as $val) {
$val = trim($val);
@@ -445,6 +458,16 @@ public function selectCount(string $select = '', string $alias = '')
return $this->maxMinAvgSum($select, $alias, 'COUNT');
}
+ /**
+ * Adds a subquery to the selection
+ */
+ public function selectSubquery(BaseBuilder $subquery, string $as): self
+ {
+ $this->QBSelect[] = $this->buildSubquery($subquery, true, $as);
+
+ return $this;
+ }
+
/**
* SELECT [MAX|MIN|AVG|SUM|COUNT]()
*
@@ -519,41 +542,55 @@ public function distinct(bool $val = true)
*
* @return $this
*/
- public function from($from, bool $overwrite = false)
+ public function from($from, bool $overwrite = false): self
{
if ($overwrite === true) {
$this->QBFrom = [];
$this->db->setAliasedTables([]);
}
- foreach ((array) $from as $val) {
- if (strpos($val, ',') !== false) {
- foreach (explode(',', $val) as $v) {
- $v = trim($v);
- $this->trackAliases($v);
-
- $this->QBFrom[] = $this->db->protectIdentifiers($v, true, null, false);
- }
+ foreach ((array) $from as $table) {
+ if (strpos($table, ',') !== false) {
+ $this->from(explode(',', $table));
} else {
- $val = trim($val);
+ $table = trim($table);
- // Extract any aliases that might exist. We use this information
- // in the protectIdentifiers to know whether to add a table prefix
- $this->trackAliases($val);
+ if ($table === '') {
+ continue;
+ }
- $this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
+ $this->trackAliases($table);
+ $this->QBFrom[] = $this->db->protectIdentifiers($table, true, null, false);
}
}
return $this;
}
+ /**
+ * @param BaseBuilder $from Expected subquery
+ * @param string $alias Subquery alias
+ *
+ * @return $this
+ */
+ public function fromSubquery(BaseBuilder $from, string $alias): self
+ {
+ $table = $this->buildSubquery($from, true, $alias);
+
+ $this->trackAliases($table);
+ $this->QBFrom[] = $table;
+
+ return $this;
+ }
+
/**
* Generates the JOIN portion of the query
*
+ * @param RawSql|string $cond
+ *
* @return $this
*/
- public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
+ public function join(string $table, $cond, string $type = '', ?bool $escape = null)
{
if ($type !== '') {
$type = strtoupper(trim($type));
@@ -573,6 +610,17 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca
$escape = $this->db->protectIdentifiers;
}
+ // Do we want to escape the table name?
+ if ($escape === true) {
+ $table = $this->db->protectIdentifiers($table, true, null, false);
+ }
+
+ if ($cond instanceof RawSql) {
+ $this->QBJoin[] = $type . 'JOIN ' . $table . ' ON ' . $cond;
+
+ return $this;
+ }
+
if (! $this->hasOperator($cond)) {
$cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')';
} elseif ($escape === false) {
@@ -606,11 +654,6 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca
}
}
- // Do we want to escape the table name?
- if ($escape === true) {
- $table = $this->db->protectIdentifiers($table, true, null, false);
- }
-
// Assemble the JOIN statement
$this->QBJoin[] = $type . 'JOIN ' . $table . $cond;
@@ -621,8 +664,8 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca
* Generates the WHERE portion of the query.
* Separates multiple calls with 'AND'.
*
- * @param mixed $key
- * @param mixed $value
+ * @param array|RawSql|string $key
+ * @param mixed $value
*
* @return $this
*/
@@ -637,9 +680,9 @@ public function where($key, $value = null, ?bool $escape = null)
* Generates the WHERE portion of the query.
* Separates multiple calls with 'OR'.
*
- * @param mixed $key
- * @param mixed $value
- * @param bool $escape
+ * @param array|RawSql|string $key
+ * @param mixed $value
+ * @param bool $escape
*
* @return $this
*/
@@ -654,15 +697,20 @@ public function orWhere($key, $value = null, ?bool $escape = null)
* @used-by having()
* @used-by orHaving()
*
- * @param mixed $key
- * @param mixed $value
+ * @param array|RawSql|string $key
+ * @param mixed $value
*
* @return $this
*/
protected function whereHaving(string $qbKey, $key, $value = null, string $type = 'AND ', ?bool $escape = null)
{
- if (! is_array($key)) {
- $key = [$key => $value];
+ if ($key instanceof RawSql) {
+ $keyValue = [(string) $key => $key];
+ $escape = false;
+ } elseif (! is_array($key)) {
+ $keyValue = [$key => $value];
+ } else {
+ $keyValue = $key;
}
// If the escape value was not set will base it on the global setting
@@ -670,10 +718,13 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type
$escape = $this->db->protectIdentifiers;
}
- foreach ($key as $k => $v) {
+ foreach ($keyValue as $k => $v) {
$prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type);
- if ($v !== null) {
+ if ($v instanceof RawSql) {
+ $k = '';
+ $op = '';
+ } elseif ($v !== null) {
$op = $this->getOperator($k, true);
if (! empty($op)) {
@@ -709,10 +760,17 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type
$op = '';
}
- $this->{$qbKey}[] = [
- 'condition' => $prefix . $k . $op . $v,
- 'escape' => $escape,
- ];
+ if ($v instanceof RawSql) {
+ $this->{$qbKey}[] = [
+ 'condition' => $v->with($prefix . $k . $op . $v),
+ 'escape' => $escape,
+ ];
+ } else {
+ $this->{$qbKey}[] = [
+ 'condition' => $prefix . $k . $op . $v,
+ 'escape' => $escape,
+ ];
+ }
}
return $this;
@@ -889,7 +947,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal
* Generates a %LIKE% portion of the query.
* Separates multiple calls with 'AND'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -902,7 +960,7 @@ public function like($field, string $match = '', string $side = 'both', ?bool $e
* Generates a NOT LIKE portion of the query.
* Separates multiple calls with 'AND'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -915,7 +973,7 @@ public function notLike($field, string $match = '', string $side = 'both', ?bool
* Generates a %LIKE% portion of the query.
* Separates multiple calls with 'OR'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -928,7 +986,7 @@ public function orLike($field, string $match = '', string $side = 'both', ?bool
* Generates a NOT LIKE portion of the query.
* Separates multiple calls with 'OR'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -941,7 +999,7 @@ public function orNotLike($field, string $match = '', string $side = 'both', ?bo
* Generates a %LIKE% portion of the query.
* Separates multiple calls with 'AND'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -954,7 +1012,7 @@ public function havingLike($field, string $match = '', string $side = 'both', ?b
* Generates a NOT LIKE portion of the query.
* Separates multiple calls with 'AND'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -967,7 +1025,7 @@ public function notHavingLike($field, string $match = '', string $side = 'both',
* Generates a %LIKE% portion of the query.
* Separates multiple calls with 'OR'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -980,7 +1038,7 @@ public function orHavingLike($field, string $match = '', string $side = 'both',
* Generates a NOT LIKE portion of the query.
* Separates multiple calls with 'OR'.
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
@@ -999,20 +1057,50 @@ public function orNotHavingLike($field, string $match = '', string $side = 'both
* @used-by notHavingLike()
* @used-by orNotHavingLike()
*
- * @param mixed $field
+ * @param array|RawSql|string $field
*
* @return $this
*/
protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', ?bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere')
{
- if (! is_array($field)) {
- $field = [$field => $match];
- }
-
$escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
$side = strtolower($side);
- foreach ($field as $k => $v) {
+ if ($field instanceof RawSql) {
+ $k = (string) $field;
+ $v = $match;
+ $insensitiveSearch = false;
+
+ $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
+
+ if ($side === 'none') {
+ $bind = $this->setBind($field->getBindingKey(), $v, $escape);
+ } elseif ($side === 'before') {
+ $bind = $this->setBind($field->getBindingKey(), "%{$v}", $escape);
+ } elseif ($side === 'after') {
+ $bind = $this->setBind($field->getBindingKey(), "{$v}%", $escape);
+ } else {
+ $bind = $this->setBind($field->getBindingKey(), "%{$v}%", $escape);
+ }
+
+ $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
+
+ // some platforms require an escape sequence definition for LIKE wildcards
+ if ($escape === true && $this->db->likeEscapeStr !== '') {
+ $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
+ }
+
+ $this->{$clause}[] = [
+ 'condition' => $field->with($likeStatement),
+ 'escape' => $escape,
+ ];
+
+ return $this;
+ }
+
+ $keyValue = ! is_array($field) ? [$field => $match] : $field;
+
+ foreach ($keyValue as $k => $v) {
if ($insensitiveSearch === true) {
$v = strtolower($v);
}
@@ -1029,7 +1117,7 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri
$bind = $this->setBind($k, "%{$v}%", $escape);
}
- $likeStatement = $this->_like_statement($prefix, $this->db->protectIdentifiers($k, false, $escape), $not, $bind, $insensitiveSearch);
+ $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
// some platforms require an escape sequence definition for LIKE wildcards
if ($escape === true && $this->db->likeEscapeStr !== '') {
@@ -1051,12 +1139,54 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri
protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
{
if ($insensitiveSearch === true) {
- return "{$prefix} LOWER({$column}) {$not} LIKE :{$bind}:";
+ return "{$prefix} LOWER(" . $this->db->escapeIdentifiers($column) . ") {$not} LIKE :{$bind}:";
}
return "{$prefix} {$column} {$not} LIKE :{$bind}:";
}
+ /**
+ * Add UNION statement
+ *
+ * @param BaseBuilder|Closure $union
+ *
+ * @return $this
+ */
+ public function union($union)
+ {
+ return $this->addUnionStatement($union);
+ }
+
+ /**
+ * Add UNION ALL statement
+ *
+ * @param BaseBuilder|Closure $union
+ *
+ * @return $this
+ */
+ public function unionAll($union)
+ {
+ return $this->addUnionStatement($union, true);
+ }
+
+ /**
+ * @used-by union()
+ * @used-by unionAll()
+ *
+ * @param BaseBuilder|Closure $union
+ *
+ * @return $this
+ */
+ protected function addUnionStatement($union, bool $all = false)
+ {
+ $this->QBUnion[] = "\n" . 'UNION '
+ . ($all ? 'ALL ' : '')
+ . 'SELECT * FROM '
+ . $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1));
+
+ return $this;
+ }
+
/**
* Starts a query group.
*
@@ -1247,8 +1377,8 @@ public function groupBy($by, ?bool $escape = null)
/**
* Separates multiple calls with 'AND'.
*
- * @param array|string $key
- * @param mixed $value
+ * @param array|RawSql|string $key
+ * @param mixed $value
*
* @return $this
*/
@@ -1260,8 +1390,8 @@ public function having($key, $value = null, ?bool $escape = null)
/**
* Separates multiple calls with 'OR'.
*
- * @param array|string $key
- * @param mixed $value
+ * @param array|RawSql|string $key
+ * @param mixed $value
*
* @return $this
*/
@@ -1650,7 +1780,10 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
}
if (! $hasQBSet) {
- $this->resetWrite();
+ $this->resetRun([
+ 'QBSet' => [],
+ 'QBKeys' => [],
+ ]);
}
}
@@ -2314,6 +2447,8 @@ protected function compileSelect($selectOverride = false): string
if (empty($this->QBSelect)) {
$sql .= '*';
+ } elseif ($this->QBSelect[0] instanceof RawSql) {
+ $sql .= (string) $this->QBSelect[0];
} else {
// Cycle through the "select" portion of the query and prep each column name.
// The reason we protect identifiers here rather than in the select() function
@@ -2341,10 +2476,10 @@ protected function compileSelect($selectOverride = false): string
. $this->compileOrderBy();
if ($this->QBLimit) {
- return $this->_limit($sql . "\n");
+ $sql = $this->_limit($sql . "\n");
}
- return $sql;
+ return $this->unionInjection($sql);
}
/**
@@ -2382,6 +2517,12 @@ protected function compileWhereHaving(string $qbKey): string
continue;
}
+ if ($qbkey['condition'] instanceof RawSql) {
+ $qbkey = $qbkey['condition'];
+
+ continue;
+ }
+
if ($qbkey['escape'] === false) {
$qbkey = $qbkey['condition'];
@@ -2493,6 +2634,17 @@ protected function compileOrderBy(): string
return '';
}
+ protected function unionInjection(string $sql): string
+ {
+ if ($this->QBUnion === []) {
+ return $sql;
+ }
+
+ return 'SELECT * FROM (' . $sql . ') '
+ . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0')
+ . implode("\n", $this->QBUnion);
+ }
+
/**
* Takes an object as input and converts the class variables to array key/vals
*
@@ -2612,6 +2764,7 @@ protected function resetSelect()
'QBDistinct' => false,
'QBLimit' => false,
'QBOffset' => false,
+ 'QBUnion' => [],
]);
if (! empty($this->db)) {
@@ -2743,16 +2896,29 @@ protected function isSubquery($value): bool
/**
* @param BaseBuilder|Closure $builder
* @param bool $wrapped Wrap the subquery in brackets
+ * @param string $alias Subquery alias
*/
- protected function buildSubquery($builder, bool $wrapped = false): string
+ protected function buildSubquery($builder, bool $wrapped = false, string $alias = ''): string
{
if ($builder instanceof Closure) {
- $instance = (clone $this)->from([], true)->resetQuery();
- $builder = $builder($instance);
+ $builder($builder = $this->db->newQuery());
+ }
+
+ if ($builder === $this) {
+ throw new DatabaseException('The subquery cannot be the same object as the main query object.');
}
$subquery = strtr($builder->getCompiledSelect(), "\n", ' ');
- return $wrapped ? '(' . $subquery . ')' : $subquery;
+ if ($wrapped) {
+ $subquery = '(' . $subquery . ')';
+ $alias = trim($alias);
+
+ if ($alias !== '') {
+ $subquery .= ' ' . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers($alias) : $alias);
+ }
+ }
+
+ return $subquery;
}
}
diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
index 53e5dad1b351..9d7e69bf5256 100644
--- a/system/Database/BaseConnection.php
+++ b/system/Database/BaseConnection.php
@@ -14,6 +14,7 @@
use Closure;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Events\Events;
+use stdClass;
use Throwable;
/**
@@ -326,7 +327,7 @@ abstract class BaseConnection implements ConnectionInterface
*
* @var string
*/
- protected $queryClass = 'CodeIgniter\\Database\\Query';
+ protected $queryClass = Query::class;
/**
* Saves our connection settings.
@@ -342,6 +343,13 @@ public function __construct(array $params)
if (class_exists($queryClass)) {
$this->queryClass = $queryClass;
}
+
+ if ($this->failover !== []) {
+ // If there is a failover database, connect now to do failover.
+ // Otherwise, Query Builder creates SQL statement with the main database config
+ // (DBPrefix) even when the main database is down.
+ $this->initialize();
+ }
}
/**
@@ -508,7 +516,7 @@ public function getPrefix(): string
}
/**
- * The name of the platform in use (MySQLi, mssql, etc)
+ * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
*/
public function getPlatform(): string
{
@@ -854,6 +862,14 @@ public function table($tableName)
return new $className($tableName, $this);
}
+ /**
+ * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
+ */
+ public function newQuery(): BaseBuilder
+ {
+ return $this->table(',')->from([], true);
+ }
+
/**
* Creates a prepared statement with the database that can then
* be used to execute multiple statements against. Within the
@@ -954,10 +970,12 @@ public function getConnectDuration(int $decimals = 6): string
* the correct identifiers.
*
* @param array|string $item
- * @param bool $prefixSingle Prefix an item with no segments?
- * @param bool $fieldExists Supplied $item contains a field name?
+ * @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)
*/
public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
{
@@ -1013,8 +1031,7 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro
//
// NOTE: The ! empty() condition prevents this method
// from breaking when QB isn't enabled.
- $firstSegment = trim($parts[0], $this->escapeChar);
- if (! empty($this->aliasedTables) && in_array($firstSegment, $this->aliasedTables, true)) {
+ if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
if ($protectIdentifiers === true) {
foreach ($parts as $key => $val) {
if (! in_array($val, $this->reservedIdentifiers, true)) {
@@ -1429,7 +1446,7 @@ public function fieldExists(string $fieldName, string $tableName): bool
/**
* Returns an object with field data
*
- * @return array
+ * @return stdClass[]
*/
public function getFieldData(string $table)
{
@@ -1518,7 +1535,8 @@ public function isWriteType($sql): bool
*
* Must return an array with keys 'code' and 'message':
*
- * return ['code' => null, 'message' => null);
+ * @return array
+ * @phpstan-return array{code: int|string|null, message: string|null}
*/
abstract public function error(): array;
diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php
index ba36915c79fe..ce5a208a1ef3 100644
--- a/system/Database/BasePreparedQuery.php
+++ b/system/Database/BasePreparedQuery.php
@@ -57,7 +57,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface
public function __construct(BaseConnection $db)
{
- $this->db = &$db;
+ $this->db = $db;
}
/**
@@ -69,7 +69,7 @@ public function __construct(BaseConnection $db)
*
* @return mixed
*/
- public function prepare(string $sql, array $options = [], string $queryClass = 'CodeIgniter\\Database\\Query')
+ public function prepare(string $sql, array $options = [], string $queryClass = Query::class)
{
// We only supports positional placeholders (?)
// in order to work with the execute method below, so we
diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php
index 7848ae75ecf0..9ba48c927619 100644
--- a/system/Database/BaseUtils.php
+++ b/system/Database/BaseUtils.php
@@ -49,9 +49,9 @@ abstract class BaseUtils
/**
* Class constructor
*/
- public function __construct(ConnectionInterface &$db)
+ public function __construct(ConnectionInterface $db)
{
- $this->db = &$db;
+ $this->db = $db;
}
/**
diff --git a/system/Database/Config.php b/system/Database/Config.php
index 6d2e82bf1109..f73c93a1169a 100644
--- a/system/Database/Config.php
+++ b/system/Database/Config.php
@@ -55,7 +55,7 @@ public static function connect($group = null, bool $getShared = true)
$group = 'custom-' . md5(json_encode($config));
}
- $config = $config ?? config('Database');
+ $config ??= config('Database');
if (empty($group)) {
$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
diff --git a/system/Database/Forge.php b/system/Database/Forge.php
index 406f75b47030..0ffd245fa7a0 100644
--- a/system/Database/Forge.php
+++ b/system/Database/Forge.php
@@ -179,7 +179,7 @@ class Forge
*/
public function __construct(BaseConnection $db)
{
- $this->db = &$db;
+ $this->db = $db;
}
/**
@@ -804,9 +804,7 @@ protected function _alterTable(string $alterType, string $table, $fields)
$fields = explode(',', $fields);
}
- $fields = array_map(function ($field) {
- return 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field));
- }, $fields);
+ $fields = array_map(fn ($field) => 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)), $fields);
return $sql . implode(', ', $fields);
}
@@ -982,6 +980,8 @@ protected function _attributeDefault(array &$attributes, array &$field)
// Override the NULL attribute if that's our default
$attributes['NULL'] = true;
$field['null'] = empty($this->null) ? '' : ' ' . $this->null;
+ } elseif ($attributes['DEFAULT'] instanceof RawSql) {
+ $field['default'] = $this->default . $attributes['DEFAULT'];
} else {
$field['default'] = $this->default . $this->db->escape($attributes['DEFAULT']);
}
diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php
index 517688889ec6..e92d31b02450 100644
--- a/system/Database/MigrationRunner.php
+++ b/system/Database/MigrationRunner.php
@@ -40,7 +40,8 @@ class MigrationRunner
protected $table;
/**
- * The Namespace where migrations can be found.
+ * The Namespace where migrations can be found.
+ * `null` is all namespaces.
*
* @var string|null
*/
@@ -423,7 +424,7 @@ public function findNamespaceMigrations(string $namespace): array
if (! empty($this->path)) {
helper('filesystem');
$dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
- $files = get_filenames($dir, true);
+ $files = get_filenames($dir, true, false, false);
} else {
$files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
}
diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php
new file mode 100644
index 000000000000..a954ec2337ad
--- /dev/null
+++ b/system/Database/OCI8/Builder.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\Database\OCI8;
+
+use CodeIgniter\Database\BaseBuilder;
+use CodeIgniter\Database\Exceptions\DatabaseException;
+
+/**
+ * Builder for OCI8
+ */
+class Builder extends BaseBuilder
+{
+ /**
+ * Identifier escape character
+ *
+ * @var string
+ */
+ protected $escapeChar = '"';
+
+ /**
+ * ORDER BY random keyword
+ *
+ * @var array
+ */
+ protected $randomKeyword = [
+ '"DBMS_RANDOM"."RANDOM"',
+ ];
+
+ /**
+ * COUNT string
+ *
+ * @used-by CI_DB_driver::count_all()
+ * @used-by BaseBuilder::count_all_results()
+ *
+ * @var string
+ */
+ protected $countString = 'SELECT COUNT(1) ';
+
+ /**
+ * Limit used flag
+ *
+ * If we use LIMIT, we'll add a field that will
+ * throw off num_fields later.
+ *
+ * @var bool
+ */
+ protected $limitUsed = false;
+
+ /**
+ * A reference to the database connection.
+ *
+ * @var Connection
+ */
+ protected $db;
+
+ /**
+ * Generates a platform-specific insert string from the supplied data.
+ */
+ protected function _insertBatch(string $table, array $keys, array $values): string
+ {
+ $insertKeys = implode(', ', $keys);
+ $hasPrimaryKey = in_array('PRIMARY', array_column($this->db->getIndexData($table), 'type'), true);
+
+ // ORA-00001 measures
+ if ($hasPrimaryKey) {
+ $sql = 'INSERT INTO ' . $table . ' (' . $insertKeys . ") \n SELECT * FROM (\n";
+ $selectQueryValues = [];
+
+ foreach ($values as $value) {
+ $selectValues = implode(',', array_map(static fn ($value, $key) => $value . ' as ' . $key, explode(',', substr(substr($value, 1), 0, -1)), $keys));
+ $selectQueryValues[] = 'SELECT ' . $selectValues . ' FROM DUAL';
+ }
+
+ return $sql . implode("\n UNION ALL \n", $selectQueryValues) . "\n)";
+ }
+
+ $sql = "INSERT ALL\n";
+
+ foreach ($values as $value) {
+ $sql .= ' INTO ' . $table . ' (' . $insertKeys . ') VALUES ' . $value . "\n";
+ }
+
+ return $sql . 'SELECT * FROM DUAL';
+ }
+
+ /**
+ * Generates a platform-specific replace string from the supplied data
+ */
+ protected function _replace(string $table, array $keys, array $values): string
+ {
+ $fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
+
+ $uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
+ $hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
+
+ return ($index->type === 'PRIMARY') && $hasAllFields;
+ });
+ $replaceableFields = array_filter($keys, static function ($columnName) use ($uniqueIndexes) {
+ foreach ($uniqueIndexes as $index) {
+ if (in_array(trim($columnName, '"'), $index->fields, true)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ $sql = 'MERGE INTO ' . $table . "\n USING (SELECT ";
+
+ $sql .= implode(', ', array_map(static fn ($columnName, $value) => $value . ' ' . $columnName, $keys, $values));
+
+ $sql .= ' FROM DUAL) "_replace" ON ( ';
+
+ $onList = [];
+ $onList[] = '1 != 1';
+
+ foreach ($uniqueIndexes as $index) {
+ $onList[] = '(' . implode(' AND ', array_map(static fn ($columnName) => $table . '."' . $columnName . '" = "_replace"."' . $columnName . '"', $index->fields)) . ')';
+ }
+
+ $sql .= implode(' OR ', $onList) . ') WHEN MATCHED THEN UPDATE SET ';
+
+ $sql .= implode(', ', array_map(static fn ($columnName) => $columnName . ' = "_replace".' . $columnName, $replaceableFields));
+
+ $sql .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $replaceableFields) . ') VALUES ';
+
+ return $sql . (' (' . implode(', ', array_map(static fn ($columnName) => '"_replace".' . $columnName, $replaceableFields)) . ')');
+ }
+
+ /**
+ * Generates a platform-specific truncate string from the supplied data
+ *
+ * If the database does not support the truncate() command,
+ * then this method maps to 'DELETE FROM table'
+ */
+ protected function _truncate(string $table): string
+ {
+ return 'TRUNCATE TABLE ' . $table;
+ }
+
+ /**
+ * Compiles a delete string and runs the query
+ *
+ * @param mixed $where
+ *
+ * @throws DatabaseException
+ *
+ * @return mixed
+ */
+ public function delete($where = '', ?int $limit = null, bool $resetData = true)
+ {
+ if (! empty($limit)) {
+ $this->QBLimit = $limit;
+ }
+
+ return parent::delete($where, null, $resetData);
+ }
+
+ /**
+ * Generates a platform-specific delete string from the supplied data
+ */
+ protected function _delete(string $table): string
+ {
+ if ($this->QBLimit) {
+ $this->where('rownum <= ', $this->QBLimit, false);
+ $this->QBLimit = false;
+ }
+
+ return parent::_delete($table);
+ }
+
+ /**
+ * Generates a platform-specific update string from the supplied data
+ */
+ protected function _update(string $table, array $values): string
+ {
+ $valStr = [];
+
+ foreach ($values as $key => $val) {
+ $valStr[] = $key . ' = ' . $val;
+ }
+
+ if ($this->QBLimit) {
+ $this->where('rownum <= ', $this->QBLimit, false);
+ }
+
+ return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
+ . $this->compileWhereHaving('QBWhere')
+ . $this->compileOrderBy();
+ }
+
+ /**
+ * Generates a platform-specific LIMIT clause.
+ */
+ protected function _limit(string $sql, bool $offsetIgnore = false): string
+ {
+ $offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0);
+ if (version_compare($this->db->getVersion(), '12.1', '>=')) {
+ // OFFSET-FETCH can be used only with the ORDER BY clause
+ if (empty($this->QBOrderBy)) {
+ $sql .= ' ORDER BY 1';
+ }
+
+ return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY';
+ }
+
+ $this->limitUsed = true;
+ $limitTemplateQuery = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (%s) INNER_QUERY WHERE ROWNUM < %d)' . ($offset ? ' WHERE RNUM >= %d' : '');
+
+ return sprintf($limitTemplateQuery, $sql, $offset + $this->QBLimit + 1, $offset);
+ }
+
+ /**
+ * Resets the query builder values. Called by the get() function
+ */
+ protected function resetSelect()
+ {
+ $this->limitUsed = false;
+ parent::resetSelect();
+ }
+}
diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php
new file mode 100644
index 000000000000..dc0b26f57c0e
--- /dev/null
+++ b/system/Database/OCI8/Connection.php
@@ -0,0 +1,720 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Database\OCI8;
+
+use CodeIgniter\Database\BaseConnection;
+use CodeIgniter\Database\ConnectionInterface;
+use CodeIgniter\Database\Exceptions\DatabaseException;
+use CodeIgniter\Database\Query;
+use ErrorException;
+use stdClass;
+
+/**
+ * Connection for OCI8
+ */
+class Connection extends BaseConnection implements ConnectionInterface
+{
+ /**
+ * Database driver
+ *
+ * @var string
+ */
+ protected $DBDriver = 'OCI8';
+
+ /**
+ * Identifier escape character
+ *
+ * @var string
+ */
+ public $escapeChar = '"';
+
+ /**
+ * List of reserved identifiers
+ *
+ * Identifiers that must NOT be escaped.
+ *
+ * @var array
+ */
+ protected $reservedIdentifiers = [
+ '*',
+ 'rownum',
+ ];
+
+ protected $validDSNs = [
+ 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS
+ // Easy Connect string (Oracle 10g+)
+ 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i',
+ 'in' => '/^[a-z0-9$_]+$/i', // Instance name (defined in tnsnames.ora)
+ ];
+
+ /**
+ * Reset $stmtId flag
+ *
+ * Used by storedProcedure() to prevent execute() from
+ * re-setting the statement ID.
+ */
+ protected $resetStmtId = true;
+
+ /**
+ * Statement ID
+ *
+ * @var resource
+ */
+ protected $stmtId;
+
+ /**
+ * Commit mode flag
+ *
+ * @used-by PreparedQuery::_execute()
+ *
+ * @var int
+ */
+ public $commitMode = OCI_COMMIT_ON_SUCCESS;
+
+ /**
+ * Cursor ID
+ *
+ * @var resource
+ */
+ protected $cursorId;
+
+ /**
+ * Latest inserted table name.
+ *
+ * @used-by PreparedQuery::_execute()
+ *
+ * @var string|null
+ */
+ public $lastInsertedTableName;
+
+ /**
+ * confirm DNS format.
+ */
+ private function isValidDSN(): bool
+ {
+ foreach ($this->validDSNs as $regexp) {
+ if (preg_match($regexp, $this->DSN)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Connect to the database.
+ *
+ * @return mixed
+ */
+ public function connect(bool $persistent = false)
+ {
+ if (empty($this->DSN) && ! $this->isValidDSN()) {
+ $this->buildDSN();
+ }
+
+ $func = $persistent ? 'oci_pconnect' : 'oci_connect';
+
+ return empty($this->charset)
+ ? $func($this->username, $this->password, $this->DSN)
+ : $func($this->username, $this->password, $this->DSN, $this->charset);
+ }
+
+ /**
+ * Keep or establish the connection if no queries have been sent for
+ * a length of time exceeding the server's idle timeout.
+ *
+ * @return void
+ */
+ public function reconnect()
+ {
+ }
+
+ /**
+ * Close the database connection.
+ *
+ * @return void
+ */
+ protected function _close()
+ {
+ if (is_resource($this->cursorId)) {
+ oci_free_statement($this->cursorId);
+ }
+ if (is_resource($this->stmtId)) {
+ oci_free_statement($this->stmtId);
+ }
+ oci_close($this->connID);
+ }
+
+ /**
+ * Select a specific database table to use.
+ */
+ public function setDatabase(string $databaseName): bool
+ {
+ return false;
+ }
+
+ /**
+ * Returns a string containing the version of the database being used.
+ */
+ public function getVersion(): string
+ {
+ if (isset($this->dataCache['version'])) {
+ return $this->dataCache['version'];
+ }
+
+ if (! $this->connID || ($versionString = oci_server_version($this->connID)) === false) {
+ return '';
+ }
+ if (preg_match('#Release\s(\d+(?:\.\d+)+)#', $versionString, $match)) {
+ return $this->dataCache['version'] = $match[1];
+ }
+
+ return '';
+ }
+
+ /**
+ * Executes the query against the database.
+ *
+ * @return false|resource
+ */
+ protected function execute(string $sql)
+ {
+ try {
+ if ($this->resetStmtId === true) {
+ $this->stmtId = oci_parse($this->connID, $sql);
+ }
+
+ oci_set_prefetch($this->stmtId, 1000);
+
+ $result = oci_execute($this->stmtId, $this->commitMode) ? $this->stmtId : false;
+ $insertTableName = $this->parseInsertTableName($sql);
+
+ if ($result && $insertTableName !== '') {
+ $this->lastInsertedTableName = $insertTableName;
+ }
+
+ return $result;
+ } catch (ErrorException $e) {
+ log_message('error', $e->getMessage());
+
+ if ($this->DBDebug) {
+ throw $e;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the table name for the insert statement from sql.
+ */
+ public function parseInsertTableName(string $sql): string
+ {
+ $commentStrippedSql = preg_replace(['/\/\*(.|\n)*?\*\//m', '/--.+/'], '', $sql);
+ $isInsertQuery = strpos(strtoupper(ltrim($commentStrippedSql)), 'INSERT') === 0;
+
+ if (! $isInsertQuery) {
+ return '';
+ }
+
+ preg_match('/(?is)\b(?:into)\s+("?\w+"?)/', $commentStrippedSql, $match);
+ $tableName = $match[1] ?? '';
+
+ return strpos($tableName, '"') === 0 ? trim($tableName, '"') : strtoupper($tableName);
+ }
+
+ /**
+ * Returns the total number of rows affected by this query.
+ */
+ public function affectedRows(): int
+ {
+ return oci_num_rows($this->stmtId);
+ }
+
+ /**
+ * Generates the SQL for listing tables in a platform-dependent manner.
+ */
+ protected function _listTables(bool $prefixLimit = false): string
+ {
+ $sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"';
+
+ if ($prefixLimit !== false && $this->DBPrefix !== '') {
+ return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' "
+ . sprintf($this->likeEscapeStr, $this->likeEscapeChar);
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Generates a platform-specific query string so that the column names can be fetched.
+ */
+ protected function _listColumns(string $table = ''): string
+ {
+ if (strpos($table, '.') !== false) {
+ sscanf($table, '%[^.].%s', $owner, $table);
+ } else {
+ $owner = $this->username;
+ }
+
+ return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
+ WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . '
+ AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($this->DBPrefix . $table));
+ }
+
+ /**
+ * Returns an array of objects with field data
+ *
+ * @throws DatabaseException
+ *
+ * @return stdClass[]
+ */
+ protected function _fieldData(string $table): array
+ {
+ if (strpos($table, '.') !== false) {
+ sscanf($table, '%[^.].%s', $owner, $table);
+ } else {
+ $owner = $this->username;
+ }
+
+ $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE
+ FROM ALL_TAB_COLUMNS
+ WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . '
+ AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($table));
+
+ if (($query = $this->query($sql)) === false) {
+ throw new DatabaseException(lang('Database.failGetFieldData'));
+ }
+ $query = $query->getResultObject();
+
+ $retval = [];
+
+ for ($i = 0, $c = count($query); $i < $c; $i++) {
+ $retval[$i] = new stdClass();
+ $retval[$i]->name = $query[$i]->COLUMN_NAME;
+ $retval[$i]->type = $query[$i]->DATA_TYPE;
+
+ $length = $query[$i]->CHAR_LENGTH > 0 ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION;
+ $length ??= $query[$i]->DATA_LENGTH;
+
+ $retval[$i]->max_length = $length;
+
+ $default = $query[$i]->DATA_DEFAULT;
+ if ($default === null && $query[$i]->NULLABLE === 'N') {
+ $default = '';
+ }
+ $retval[$i]->default = $default;
+ $retval[$i]->nullable = $query[$i]->NULLABLE === 'Y';
+ }
+
+ return $retval;
+ }
+
+ /**
+ * Returns an array of objects with index data
+ *
+ * @throws DatabaseException
+ *
+ * @return stdClass[]
+ */
+ protected function _indexData(string $table): array
+ {
+ if (strpos($table, '.') !== false) {
+ sscanf($table, '%[^.].%s', $owner, $table);
+ } else {
+ $owner = $this->username;
+ }
+
+ $sql = 'SELECT AIC.INDEX_NAME, UC.CONSTRAINT_TYPE, AIC.COLUMN_NAME '
+ . ' FROM ALL_IND_COLUMNS AIC '
+ . ' LEFT JOIN USER_CONSTRAINTS UC ON AIC.INDEX_NAME = UC.CONSTRAINT_NAME AND AIC.TABLE_NAME = UC.TABLE_NAME '
+ . 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtolower($table)) . ' '
+ . 'AND AIC.TABLE_OWNER = ' . $this->escape(strtoupper($owner)) . ' '
+ . ' ORDER BY UC.CONSTRAINT_TYPE, AIC.COLUMN_POSITION';
+
+ if (($query = $this->query($sql)) === false) {
+ throw new DatabaseException(lang('Database.failGetIndexData'));
+ }
+ $query = $query->getResultObject();
+
+ $retVal = [];
+ $constraintTypes = [
+ 'P' => 'PRIMARY',
+ 'U' => 'UNIQUE',
+ ];
+
+ foreach ($query as $row) {
+ if (isset($retVal[$row->INDEX_NAME])) {
+ $retVal[$row->INDEX_NAME]->fields[] = $row->COLUMN_NAME;
+
+ continue;
+ }
+
+ $retVal[$row->INDEX_NAME] = new stdClass();
+ $retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME;
+ $retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME];
+ $retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX';
+ }
+
+ return $retVal;
+ }
+
+ /**
+ * Returns an array of objects with Foreign key data
+ *
+ * @throws DatabaseException
+ *
+ * @return stdClass[]
+ */
+ protected function _foreignKeyData(string $table): array
+ {
+ $sql = 'SELECT
+ acc.constraint_name,
+ acc.table_name,
+ acc.column_name,
+ ccu.table_name foreign_table_name,
+ accu.column_name foreign_column_name
+ FROM all_cons_columns acc
+ JOIN all_constraints ac
+ ON acc.owner = ac.owner
+ AND acc.constraint_name = ac.constraint_name
+ JOIN all_constraints ccu
+ ON ac.r_owner = ccu.owner
+ AND ac.r_constraint_name = ccu.constraint_name
+ JOIN all_cons_columns accu
+ ON accu.constraint_name = ccu.constraint_name
+ AND accu.table_name = ccu.table_name
+ WHERE ac.constraint_type = ' . $this->escape('R') . '
+ AND acc.table_name = ' . $this->escape($table);
+ $query = $this->query($sql);
+
+ if ($query === false) {
+ throw new DatabaseException(lang('Database.failGetForeignKeyData'));
+ }
+ $query = $query->getResultObject();
+
+ $retVal = [];
+
+ foreach ($query as $row) {
+ $obj = new stdClass();
+ $obj->constraint_name = $row->CONSTRAINT_NAME;
+ $obj->table_name = $row->TABLE_NAME;
+ $obj->column_name = $row->COLUMN_NAME;
+ $obj->foreign_table_name = $row->FOREIGN_TABLE_NAME;
+ $obj->foreign_column_name = $row->FOREIGN_COLUMN_NAME;
+ $retVal[] = $obj;
+ }
+
+ return $retVal;
+ }
+
+ /**
+ * Returns platform-specific SQL to disable foreign key checks.
+ *
+ * @return string
+ */
+ protected function _disableForeignKeyChecks()
+ {
+ return <<<'SQL'
+ BEGIN
+ FOR c IN
+ (SELECT c.owner, c.table_name, c.constraint_name
+ FROM user_constraints c, user_tables t
+ WHERE c.table_name = t.table_name
+ AND c.status = 'ENABLED'
+ AND c.constraint_type = 'R'
+ AND t.iot_type IS NULL
+ ORDER BY c.constraint_type DESC)
+ LOOP
+ dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" disable constraint "' || c.constraint_name || '"');
+ END LOOP;
+ END;
+ SQL;
+ }
+
+ /**
+ * Returns platform-specific SQL to enable foreign key checks.
+ *
+ * @return string
+ */
+ protected function _enableForeignKeyChecks()
+ {
+ return <<<'SQL'
+ BEGIN
+ FOR c IN
+ (SELECT c.owner, c.table_name, c.constraint_name
+ FROM user_constraints c, user_tables t
+ WHERE c.table_name = t.table_name
+ AND c.status = 'DISABLED'
+ AND c.constraint_type = 'R'
+ AND t.iot_type IS NULL
+ ORDER BY c.constraint_type DESC)
+ LOOP
+ dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" enable constraint "' || c.constraint_name || '"');
+ END LOOP;
+ END;
+ SQL;
+ }
+
+ /**
+ * Get cursor. Returns a cursor from the database
+ *
+ * @return resource
+ */
+ public function getCursor()
+ {
+ return $this->cursorId = oci_new_cursor($this->connID);
+ }
+
+ /**
+ * Executes a stored procedure
+ *
+ * @param string $procedureName procedure name to execute
+ * @param array $params params array keys
+ * KEY OPTIONAL NOTES
+ * name no the name of the parameter should be in : format
+ * value no the value of the parameter. If this is an OUT or IN OUT parameter,
+ * this should be a reference to a variable
+ * type yes the type of the parameter
+ * length yes the max size of the parameter
+ *
+ * @return bool|Query|Result
+ */
+ public function storedProcedure(string $procedureName, array $params)
+ {
+ if ($procedureName === '') {
+ throw new DatabaseException(lang('Database.invalidArgument', [$procedureName]));
+ }
+
+ // Build the query string
+ $sql = sprintf(
+ 'BEGIN %s (' . substr(str_repeat(',%s', count($params)), 1) . '); END;',
+ $procedureName,
+ ...array_map(static fn ($row) => $row['name'], $params)
+ );
+
+ $this->resetStmtId = false;
+ $this->stmtId = oci_parse($this->connID, $sql);
+ $this->bindParams($params);
+ $result = $this->query($sql);
+ $this->resetStmtId = true;
+
+ return $result;
+ }
+
+ /**
+ * Bind parameters
+ *
+ * @param array $params
+ *
+ * @return void
+ */
+ protected function bindParams($params)
+ {
+ if (! is_array($params) || ! is_resource($this->stmtId)) {
+ return;
+ }
+
+ foreach ($params as $param) {
+ oci_bind_by_name(
+ $this->stmtId,
+ $param['name'],
+ $param['value'],
+ $param['length'] ?? -1,
+ $param['type'] ?? SQLT_CHR
+ );
+ }
+ }
+
+ /**
+ * Returns the last error code and message.
+ *
+ * Must return an array with keys 'code' and 'message':
+ *
+ * return ['code' => null, 'message' => null);
+ */
+ public function error(): array
+ {
+ // oci_error() returns an array that already contains
+ // 'code' and 'message' keys, but it can return false
+ // if there was no error ....
+ $error = oci_error();
+ $resources = [$this->cursorId, $this->stmtId, $this->connID];
+
+ foreach ($resources as $resource) {
+ if (is_resource($resource)) {
+ $error = oci_error($resource);
+ break;
+ }
+ }
+
+ return is_array($error)
+ ? $error
+ : [
+ 'code' => '',
+ 'message' => '',
+ ];
+ }
+
+ public function insertID(): int
+ {
+ if (empty($this->lastInsertedTableName)) {
+ return 0;
+ }
+
+ $indexs = $this->getIndexData($this->lastInsertedTableName);
+ $fieldDatas = $this->getFieldData($this->lastInsertedTableName);
+
+ if (! $indexs || ! $fieldDatas) {
+ return 0;
+ }
+
+ $columnTypeList = array_column($fieldDatas, 'type', 'name');
+ $primaryColumnName = '';
+
+ foreach ($indexs as $index) {
+ if ($index->type !== 'PRIMARY' || count($index->fields) !== 1) {
+ continue;
+ }
+
+ $primaryColumnName = $this->protectIdentifiers($index->fields[0], false, false);
+ $primaryColumnType = $columnTypeList[$primaryColumnName];
+
+ if ($primaryColumnType !== 'NUMBER') {
+ $primaryColumnName = '';
+ }
+ }
+
+ if (! $primaryColumnName) {
+ return 0;
+ }
+
+ $query = $this->query('SELECT DATA_DEFAULT FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?', [$this->lastInsertedTableName, $primaryColumnName])->getRow();
+ $lastInsertValue = str_replace('nextval', 'currval', $query->DATA_DEFAULT ?? '0');
+ $query = $this->query(sprintf('SELECT %s SEQ FROM DUAL', $lastInsertValue))->getRow();
+
+ return (int) ($query->SEQ ?? 0);
+ }
+
+ /**
+ * Build a DSN from the provided parameters
+ *
+ * @return void
+ */
+ protected function buildDSN()
+ {
+ if ($this->DSN !== '') {
+ $this->DSN = '';
+ }
+
+ // Legacy support for TNS in the hostname configuration field
+ $this->hostname = str_replace(["\n", "\r", "\t", ' '], '', $this->hostname);
+
+ if (preg_match($this->validDSNs['tns'], $this->hostname)) {
+ $this->DSN = $this->hostname;
+
+ return;
+ }
+
+ $isEasyConnectableHostName = $this->hostname !== '' && strpos($this->hostname, '/') === false && strpos($this->hostname, ':') === false;
+ $easyConnectablePort = ! empty($this->port) && ctype_digit($this->port) ? ':' . $this->port : '';
+ $easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : '';
+
+ if ($isEasyConnectableHostName && ($easyConnectablePort !== '' || $easyConnectableDatabase !== '')) {
+ /* If the hostname field isn't empty, doesn't contain
+ * ':' and/or '/' and if port and/or database aren't
+ * empty, then the hostname field is most likely indeed
+ * just a hostname. Therefore we'll try and build an
+ * Easy Connect string from these 3 settings, assuming
+ * that the database field is a service name.
+ */
+ $this->DSN = $this->hostname . $easyConnectablePort . $easyConnectableDatabase;
+
+ if (preg_match($this->validDSNs['ec'], $this->DSN)) {
+ return;
+ }
+ }
+
+ /* At this point, we can only try and validate the hostname and
+ * database fields separately as DSNs.
+ */
+ if (preg_match($this->validDSNs['ec'], $this->hostname) || preg_match($this->validDSNs['in'], $this->hostname)) {
+ $this->DSN = $this->hostname;
+
+ return;
+ }
+
+ $this->database = str_replace(["\n", "\r", "\t", ' '], '', $this->database);
+
+ foreach ($this->validDSNs as $regexp) {
+ if (preg_match($regexp, $this->database)) {
+ return;
+ }
+ }
+
+ /* Well - OK, an empty string should work as well.
+ * PHP will try to use environment variables to
+ * determine which Oracle instance to connect to.
+ */
+ $this->DSN = '';
+ }
+
+ /**
+ * Begin Transaction
+ */
+ protected function _transBegin(): bool
+ {
+ $this->commitMode = OCI_NO_AUTO_COMMIT;
+
+ return true;
+ }
+
+ /**
+ * Commit Transaction
+ */
+ protected function _transCommit(): bool
+ {
+ $this->commitMode = OCI_COMMIT_ON_SUCCESS;
+
+ return oci_commit($this->connID);
+ }
+
+ /**
+ * Rollback Transaction
+ */
+ protected function _transRollback(): bool
+ {
+ $this->commitMode = OCI_COMMIT_ON_SUCCESS;
+
+ return oci_rollback($this->connID);
+ }
+
+ /**
+ * Returns the name of the current database being used.
+ */
+ public function getDatabase(): string
+ {
+ if (! empty($this->database)) {
+ return $this->database;
+ }
+
+ return $this->query('SELECT DEFAULT_TABLESPACE FROM USER_USERS')->getRow()->DEFAULT_TABLESPACE ?? '';
+ }
+
+ /**
+ * Get the prefix of the function to access the DB.
+ */
+ protected function getDriverFunctionPrefix(): string
+ {
+ return 'oci_';
+ }
+}
diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php
new file mode 100644
index 000000000000..9cd5bad945cd
--- /dev/null
+++ b/system/Database/OCI8/Forge.php
@@ -0,0 +1,301 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Database\OCI8;
+
+use CodeIgniter\Database\Forge as BaseForge;
+
+/**
+ * Forge for OCI8
+ */
+class Forge extends BaseForge
+{
+ /**
+ * DROP INDEX statement
+ *
+ * @var string
+ */
+ protected $dropIndexStr = 'DROP INDEX %s';
+
+ /**
+ * CREATE DATABASE statement
+ *
+ * @var false
+ */
+ protected $createDatabaseStr = false;
+
+ /**
+ * CREATE TABLE IF statement
+ *
+ * @var false
+ */
+ protected $createTableIfStr = false;
+
+ /**
+ * DROP TABLE IF EXISTS statement
+ *
+ * @var false
+ */
+ protected $dropTableIfStr = false;
+
+ /**
+ * DROP DATABASE statement
+ *
+ * @var false
+ */
+ protected $dropDatabaseStr = false;
+
+ /**
+ * UNSIGNED support
+ *
+ * @var array|bool
+ */
+ protected $unsigned = false;
+
+ /**
+ * NULL value representation in CREATE/ALTER TABLE statements
+ *
+ * @var string
+ */
+ protected $null = 'NULL';
+
+ /**
+ * RENAME TABLE statement
+ *
+ * @var string
+ */
+ protected $renameTableStr = 'ALTER TABLE %s RENAME TO %s';
+
+ /**
+ * DROP CONSTRAINT statement
+ *
+ * @var string
+ */
+ protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
+
+ /**
+ * ALTER TABLE
+ *
+ * @param string $alterType ALTER type
+ * @param string $table Table name
+ * @param mixed $field Column definition
+ *
+ * @return string|string[]
+ */
+ protected function _alterTable(string $alterType, string $table, $field)
+ {
+ $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
+
+ if ($alterType === 'DROP') {
+ $fields = array_map(fn ($field) => $this->db->escapeIdentifiers(trim($field)), is_string($field) ? explode(',', $field) : $field);
+
+ return $sql . ' DROP (' . implode(',', $fields) . ') CASCADE CONSTRAINT INVALIDATE';
+ }
+ if ($alterType === 'CHANGE') {
+ $alterType = 'MODIFY';
+ }
+
+ $nullableMap = array_column($this->db->getFieldData($table), 'nullable', 'name');
+ $sqls = [];
+
+ for ($i = 0, $c = count($field); $i < $c; $i++) {
+ if ($alterType === 'MODIFY') {
+ // If a null constraint is added to a column with a null constraint,
+ // ORA-01451 will occur,
+ // so add null constraint is used only when it is different from the current null constraint.
+ $isWantToAddNull = strpos($field[$i]['null'], ' NOT') === false;
+ $currentNullAddable = $nullableMap[$field[$i]['name']];
+
+ if ($isWantToAddNull === $currentNullAddable) {
+ $field[$i]['null'] = '';
+ }
+ }
+
+ if ($field[$i]['_literal'] !== false) {
+ $field[$i] = "\n\t" . $field[$i]['_literal'];
+ } else {
+ $field[$i]['_literal'] = "\n\t" . $this->_processColumn($field[$i]);
+
+ if (! empty($field[$i]['comment'])) {
+ $sqls[] = 'COMMENT ON COLUMN '
+ . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($field[$i]['name'])
+ . ' IS ' . $field[$i]['comment'];
+ }
+
+ if ($alterType === 'MODIFY' && ! empty($field[$i]['new_name'])) {
+ $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name'])
+ . ' TO ' . $this->db->escapeIdentifiers($field[$i]['new_name']);
+ }
+
+ $field[$i] = "\n\t" . $field[$i]['_literal'];
+ }
+ }
+
+ $sql .= ' ' . $alterType . ' ';
+ $sql .= count($field) === 1
+ ? $field[0]
+ : '(' . implode(',', $field) . ')';
+
+ // RENAME COLUMN must be executed after MODIFY
+ array_unshift($sqls, $sql);
+
+ return $sqls;
+ }
+
+ /**
+ * Field attribute AUTO_INCREMENT
+ *
+ * @return void
+ */
+ protected function _attributeAutoIncrement(array &$attributes, array &$field)
+ {
+ if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
+ && stripos($field['type'], 'NUMBER') !== false
+ && version_compare($this->db->getVersion(), '12.1', '>=')
+ ) {
+ $field['auto_increment'] = ' GENERATED BY DEFAULT AS IDENTITY';
+ }
+ }
+
+ /**
+ * Process column
+ */
+ protected function _processColumn(array $field): string
+ {
+ $constraint = '';
+ // @todo: can’t cover multi pattern when set type.
+ if ($field['type'] === 'VARCHAR2' && strpos($field['length'], "('") === 0) {
+ $constraint = ' CHECK(' . $this->db->escapeIdentifiers($field['name'])
+ . ' IN ' . $field['length'] . ')';
+
+ $field['length'] = '(' . max(array_map('mb_strlen', explode("','", mb_substr($field['length'], 2, -2)))) . ')' . $constraint;
+ } elseif (count($this->primaryKeys) === 1 && $field['name'] === $this->primaryKeys[0]) {
+ $field['unique'] = '';
+ }
+
+ return $this->db->escapeIdentifiers($field['name'])
+ . ' ' . $field['type'] . $field['length']
+ . $field['unsigned']
+ . $field['default']
+ . $field['auto_increment']
+ . $field['null']
+ . $field['unique'];
+ }
+
+ /**
+ * Performs a data type mapping between different databases.
+ *
+ * @return void
+ */
+ protected function _attributeType(array &$attributes)
+ {
+ // Reset field lengths for data types that don't support it
+ // Usually overridden by drivers
+ switch (strtoupper($attributes['TYPE'])) {
+ case 'TINYINT':
+ $attributes['CONSTRAINT'] ??= 3;
+ // no break
+ case 'SMALLINT':
+ $attributes['CONSTRAINT'] ??= 5;
+ // no break
+ case 'MEDIUMINT':
+ $attributes['CONSTRAINT'] ??= 7;
+ // no break
+ case 'INT':
+ case 'INTEGER':
+ $attributes['CONSTRAINT'] ??= 11;
+ // no break
+ case 'BIGINT':
+ $attributes['CONSTRAINT'] ??= 19;
+ // no break
+ case 'NUMERIC':
+ $attributes['TYPE'] = 'NUMBER';
+
+ return;
+
+ case 'BOOLEAN':
+ $attributes['TYPE'] = 'NUMBER';
+ $attributes['CONSTRAINT'] = 1;
+ $attributes['UNSIGNED'] = true;
+ $attributes['NULL'] = false;
+
+ return;
+
+ case 'DOUBLE':
+ $attributes['TYPE'] = 'FLOAT';
+ $attributes['CONSTRAINT'] ??= 126;
+
+ return;
+
+ case 'DATETIME':
+ case 'TIME':
+ $attributes['TYPE'] = 'DATE';
+
+ return;
+
+ case 'SET':
+ case 'ENUM':
+ case 'VARCHAR':
+ $attributes['CONSTRAINT'] ??= 255;
+ // no break
+ case 'TEXT':
+ case 'MEDIUMTEXT':
+ $attributes['CONSTRAINT'] ??= 4000;
+ $attributes['TYPE'] = 'VARCHAR2';
+ }
+ }
+
+ /**
+ * Generates a platform-specific DROP TABLE string
+ *
+ * @return bool|string
+ */
+ protected function _dropTable(string $table, bool $ifExists, bool $cascade)
+ {
+ $sql = parent::_dropTable($table, $ifExists, $cascade);
+
+ if ($sql !== true && $cascade === true) {
+ $sql .= ' CASCADE CONSTRAINTS PURGE';
+ } elseif ($sql !== true) {
+ $sql .= ' PURGE';
+ }
+
+ return $sql;
+ }
+
+ protected function _processForeignKeys(string $table): string
+ {
+ $sql = '';
+
+ $allowActions = [
+ 'CASCADE',
+ 'SET NULL',
+ 'NO ACTION',
+ ];
+
+ foreach ($this->foreignKeys as $fkey) {
+ $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_fk';
+ $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex);
+ $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
+ $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
+ $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
+
+ $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)";
+ $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
+
+ if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) {
+ $sql .= ' ON DELETE ' . $fkey['onDelete'];
+ }
+ }
+
+ return $sql;
+ }
+}
diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php
new file mode 100644
index 000000000000..311dacc4045f
--- /dev/null
+++ b/system/Database/OCI8/PreparedQuery.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Database\OCI8;
+
+use BadMethodCallException;
+use CodeIgniter\Database\BasePreparedQuery;
+use CodeIgniter\Database\PreparedQueryInterface;
+
+/**
+ * Prepared query for OCI8
+ */
+class PreparedQuery extends BasePreparedQuery implements PreparedQueryInterface
+{
+ /**
+ * A reference to the db connection to use.
+ *
+ * @var Connection
+ */
+ protected $db;
+
+ /**
+ * Latest inserted table name.
+ */
+ private ?string $lastInsertTableName = null;
+
+ /**
+ * Prepares the query against the database, and saves the connection
+ * info necessary to execute the query later.
+ *
+ * NOTE: This version is based on SQL code. Child classes should
+ * override this method.
+ *
+ * @param array $options Passed to the connection's prepare statement.
+ * Unused in the OCI8 driver.
+ *
+ * @return mixed
+ */
+ public function _prepare(string $sql, array $options = [])
+ {
+ if (! $this->statement = oci_parse($this->db->connID, $this->parameterize($sql))) {
+ $error = oci_error($this->db->connID);
+ $this->errorCode = $error['code'] ?? 0;
+ $this->errorString = $error['message'] ?? '';
+ }
+
+ $this->lastInsertTableName = $this->db->parseInsertTableName($sql);
+
+ return $this;
+ }
+
+ /**
+ * Takes a new set of data and runs it against the currently
+ * prepared query. Upon success, will return a Results object.
+ */
+ public function _execute(array $data): bool
+ {
+ if (null === $this->statement) {
+ throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
+ }
+
+ $lastKey = 0;
+
+ foreach (array_keys($data) as $key) {
+ oci_bind_by_name($this->statement, ':' . $key, $data[$key]);
+ $lastKey = $key;
+ }
+
+ $result = oci_execute($this->statement, $this->db->commitMode);
+
+ if ($result && $this->lastInsertTableName !== '') {
+ $this->db->lastInsertedTableName = $this->lastInsertTableName;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the result object for the prepared query.
+ *
+ * @return mixed
+ */
+ public function _getResult()
+ {
+ return $this->statement;
+ }
+
+ /**
+ * Replaces the ? placeholders with :0, :1, etc parameters for use
+ * within the prepared query.
+ */
+ public function parameterize(string $sql): string
+ {
+ // Track our current value
+ $count = 0;
+
+ return preg_replace_callback('/\?/', static function ($matches) use (&$count) {
+ return ':' . ($count++);
+ }, $sql);
+ }
+}
diff --git a/system/Database/OCI8/Result.php b/system/Database/OCI8/Result.php
new file mode 100644
index 000000000000..72a1b0980a28
--- /dev/null
+++ b/system/Database/OCI8/Result.php
@@ -0,0 +1,115 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Database\OCI8;
+
+use CodeIgniter\Database\BaseResult;
+use CodeIgniter\Database\ResultInterface;
+use CodeIgniter\Entity;
+
+/**
+ * Result for OCI8
+ */
+class Result extends BaseResult implements ResultInterface
+{
+ /**
+ * Gets the number of fields in the result set.
+ */
+ public function getFieldCount(): int
+ {
+ return oci_num_fields($this->resultID);
+ }
+
+ /**
+ * Generates an array of column names in the result set.
+ */
+ public function getFieldNames(): array
+ {
+ return array_map(fn ($fieldIndex) => oci_field_name($this->resultID, $fieldIndex), range(1, $this->getFieldCount()));
+ }
+
+ /**
+ * Generates an array of objects representing field meta-data.
+ */
+ public function getFieldData(): array
+ {
+ return array_map(fn ($fieldIndex) => (object) [
+ 'name' => oci_field_name($this->resultID, $fieldIndex),
+ 'type' => oci_field_type($this->resultID, $fieldIndex),
+ 'max_length' => oci_field_size($this->resultID, $fieldIndex),
+ ], range(1, $this->getFieldCount()));
+ }
+
+ /**
+ * Frees the current result.
+ *
+ * @return void
+ */
+ public function freeResult()
+ {
+ if (is_resource($this->resultID)) {
+ oci_free_statement($this->resultID);
+ $this->resultID = false;
+ }
+ }
+
+ /**
+ * Moves the internal pointer to the desired offset. This is called
+ * internally before fetching results to make sure the result set
+ * starts at zero.
+ *
+ * @return false
+ */
+ public function dataSeek(int $n = 0)
+ {
+ // We can't support data seek by oci
+ return false;
+ }
+
+ /**
+ * Returns the result set as an array.
+ *
+ * Overridden by driver classes.
+ *
+ * @return mixed
+ */
+ protected function fetchAssoc()
+ {
+ return oci_fetch_assoc($this->resultID);
+ }
+
+ /**
+ * Returns the result set as an object.
+ *
+ * Overridden by child classes.
+ *
+ * @return bool|Entity|object
+ */
+ protected function fetchObject(string $className = 'stdClass')
+ {
+ $row = oci_fetch_object($this->resultID);
+
+ if ($className === 'stdClass' || ! $row) {
+ return $row;
+ }
+ if (is_subclass_of($className, Entity::class)) {
+ return (new $className())->setAttributes((array) $row);
+ }
+
+ $instance = new $className();
+
+ foreach (get_object_vars($row) as $key => $value) {
+ $instance->{$key} = $value;
+ }
+
+ return $instance;
+ }
+}
diff --git a/system/Database/OCI8/Utils.php b/system/Database/OCI8/Utils.php
new file mode 100644
index 000000000000..870306d8b8b1
--- /dev/null
+++ b/system/Database/OCI8/Utils.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Database\OCI8;
+
+use CodeIgniter\Database\BaseUtils;
+use CodeIgniter\Database\Exceptions\DatabaseException;
+
+/**
+ * Utils for OCI8
+ */
+class Utils extends BaseUtils
+{
+ /**
+ * List databases statement
+ *
+ * @var string
+ */
+ protected $listDatabases = 'SELECT TABLESPACE_NAME FROM USER_TABLESPACES';
+
+ /**
+ * Platform dependent version of the backup function.
+ *
+ * @return mixed
+ */
+ public function _backup(?array $prefs = null)
+ {
+ throw new DatabaseException('Unsupported feature of the database platform you are using.');
+ }
+}
diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php
index 1006ab20012e..1d80f687568c 100644
--- a/system/Database/Postgre/Builder.php
+++ b/system/Database/Postgre/Builder.php
@@ -13,6 +13,7 @@
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
+use CodeIgniter\Database\RawSql;
/**
* Builder for Postgre
@@ -241,7 +242,7 @@ protected function _updateBatch(string $table, array $values, string $index): st
foreach (array_keys($val) as $field) {
if ($field !== $index) {
- $final[$field] = $final[$field] ?? [];
+ $final[$field] ??= [];
$final[$field][] = "WHEN {$val[$index]} THEN {$val[$field]}";
}
@@ -300,9 +301,11 @@ protected function _like_statement(?string $prefix, string $column, ?string $not
/**
* Generates the JOIN portion of the query
*
+ * @param RawSql|string $cond
+ *
* @return BaseBuilder
*/
- public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
+ public function join(string $table, $cond, string $type = '', ?bool $escape = null)
{
if (! in_array('FULL OUTER', $this->joinTypes, true)) {
$this->joinTypes = array_merge($this->joinTypes, ['FULL OUTER']);
diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php
index 57b4c78fd749..6827ba70b54f 100644
--- a/system/Database/Postgre/Connection.php
+++ b/system/Database/Postgre/Connection.php
@@ -234,9 +234,9 @@ protected function _listColumns(string $table = ''): string
*/
protected function _fieldData(string $table): array
{
- $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default"
- FROM "information_schema"."columns"
- WHERE LOWER("table_name") = '
+ $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default", "is_nullable"
+ FROM "information_schema"."columns"
+ WHERE LOWER("table_name") = '
. $this->escape(strtolower($table))
. ' ORDER BY "ordinal_position"';
@@ -252,6 +252,7 @@ protected function _fieldData(string $table): array
$retVal[$i]->name = $query[$i]->column_name;
$retVal[$i]->type = $query[$i]->data_type;
+ $retVal[$i]->nullable = $query[$i]->is_nullable === 'YES';
$retVal[$i]->default = $query[$i]->column_default;
$retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
}
@@ -284,9 +285,7 @@ protected function _indexData(string $table): array
$obj = new stdClass();
$obj->name = $row->indexname;
$_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef)));
- $obj->fields = array_map(static function ($v) {
- return trim($v);
- }, $_fields);
+ $obj->fields = array_map(static fn ($v) => trim($v), $_fields);
if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) {
$obj->type = 'PRIMARY';
diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php
index 53e42b867f0f..890534663256 100644
--- a/system/Database/Postgre/PreparedQuery.php
+++ b/system/Database/Postgre/PreparedQuery.php
@@ -52,7 +52,7 @@ class PreparedQuery extends BasePreparedQuery
*/
public function _prepare(string $sql, array $options = [])
{
- $this->name = (string) random_int(1, 10000000000000000);
+ $this->name = (string) random_int(1, 10_000_000_000_000_000);
$sql = $this->parameterize($sql);
diff --git a/system/Database/Query.php b/system/Database/Query.php
index 91b98c77d2ef..702575ca857c 100644
--- a/system/Database/Query.php
+++ b/system/Database/Query.php
@@ -23,10 +23,17 @@ class Query implements QueryInterface
*/
protected $originalQueryString;
+ /**
+ * The query string if table prefix has been swapped.
+ *
+ * @var string|null
+ */
+ protected $swappedQueryString;
+
/**
* The final query string after binding, etc.
*
- * @var string
+ * @var string|null
*/
protected $finalQueryString;
@@ -84,7 +91,7 @@ class Query implements QueryInterface
*/
public $db;
- public function __construct(ConnectionInterface &$db)
+ public function __construct(ConnectionInterface $db)
{
$this->db = $db;
}
@@ -99,6 +106,7 @@ public function __construct(ConnectionInterface &$db)
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
{
$this->originalQueryString = $sql;
+ unset($this->swappedQueryString);
if ($binds !== null) {
if (! is_array($binds)) {
@@ -116,6 +124,8 @@ public function setQuery(string $sql, $binds = null, bool $setEscape = true)
$this->binds = $binds;
}
+ unset($this->finalQueryString);
+
return $this;
}
@@ -134,6 +144,8 @@ public function setBinds(array $binds, bool $setEscape = true)
$this->binds = $binds;
+ unset($this->finalQueryString);
+
return $this;
}
@@ -144,11 +156,9 @@ public function setBinds(array $binds, bool $setEscape = true)
public function getQuery(): string
{
if (empty($this->finalQueryString)) {
- $this->finalQueryString = $this->originalQueryString;
+ $this->compileBinds();
}
- $this->compileBinds();
-
return $this->finalQueryString;
}
@@ -251,9 +261,14 @@ public function isWriteType(): bool
*/
public function swapPrefix(string $orig, string $swap)
{
- $sql = empty($this->finalQueryString) ? $this->originalQueryString : $this->finalQueryString;
+ $sql = $this->swappedQueryString ?? $this->originalQueryString;
+
+ $from = '/(\W)' . $orig . '(\S)/';
+ $to = '\\1' . $swap . '\\2';
- $this->finalQueryString = preg_replace('/(\W)' . $orig . '(\S+?)/', '\\1' . $swap . '\\2', $sql);
+ $this->swappedQueryString = preg_replace($from, $to, $sql);
+
+ unset($this->finalQueryString);
return $this;
}
@@ -267,16 +282,18 @@ public function getOriginalQuery(): string
}
/**
- * Escapes and inserts any binds into the finalQueryString object.
+ * Escapes and inserts any binds into the finalQueryString property.
*
* @see https://regex101.com/r/EUEhay/5
*/
protected function compileBinds()
{
- $sql = $this->finalQueryString;
+ $sql = $this->swappedQueryString ?? $this->originalQueryString;
$binds = $this->binds;
if (empty($binds)) {
+ $this->finalQueryString = $sql;
+
return;
}
@@ -391,11 +408,7 @@ public function debugToolbarDisplay(): string
'WHERE',
];
- if (empty($this->finalQueryString)) {
- $this->compileBinds(); // @codeCoverageIgnore
- }
-
- $sql = esc($this->finalQueryString);
+ $sql = esc($this->getQuery());
/**
* @see https://stackoverflow.com/a/20767160
@@ -403,9 +416,7 @@ public function debugToolbarDisplay(): string
*/
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/';
- return preg_replace_callback($search, static function ($matches) {
- return '' . str_replace(' ', ' ', $matches[0]) . '';
- }, $sql);
+ return preg_replace_callback($search, static fn ($matches) => '' . str_replace(' ', ' ', $matches[0]) . '', $sql);
}
/**
diff --git a/system/Database/RawSql.php b/system/Database/RawSql.php
new file mode 100644
index 000000000000..7ecb7fd378ae
--- /dev/null
+++ b/system/Database/RawSql.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\Database;
+
+class RawSql
+{
+ /**
+ * @var string Raw SQL string
+ */
+ private string $string;
+
+ public function __construct(string $sqlString)
+ {
+ $this->string = $sqlString;
+ }
+
+ public function __toString(): string
+ {
+ return $this->string;
+ }
+
+ /**
+ * Create new instance with new SQL string
+ */
+ public function with(string $newSqlString): self
+ {
+ $new = clone $this;
+ $new->string = $newSqlString;
+
+ return $new;
+ }
+
+ /**
+ * Returns unique id for binding key
+ */
+ public function getBindingKey(): string
+ {
+ return 'RawSql' . spl_object_id($this);
+ }
+}
diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php
index 17d8bd576c9f..a5bf829a6b74 100755
--- a/system/Database/SQLSRV/Builder.php
+++ b/system/Database/SQLSRV/Builder.php
@@ -14,6 +14,7 @@
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Exceptions\DataException;
+use CodeIgniter\Database\RawSql;
use CodeIgniter\Database\ResultInterface;
/**
@@ -68,7 +69,7 @@ protected function _fromTables(): string
$from = [];
foreach ($this->QBFrom as $value) {
- $from[] = $this->getFullName($value);
+ $from[] = strpos($value, '(SELECT') === 0 ? $value : $this->getFullName($value);
}
return implode(', ', $from);
@@ -88,9 +89,11 @@ protected function _truncate(string $table): string
/**
* Generates the JOIN portion of the query
*
+ * @param RawSql|string $cond
+ *
* @return $this
*/
- public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
+ public function join(string $table, $cond, string $type = '', ?bool $escape = null)
{
if ($type !== '') {
$type = strtoupper(trim($type));
@@ -382,9 +385,7 @@ protected function _replace(string $table, array $keys, array $values): string
}
// Get the unique field names
- $escKeyFields = array_map(function (string $field): string {
- return $this->db->protectIdentifiers($field);
- }, array_values(array_unique($keyFields)));
+ $escKeyFields = array_map(fn (string $field): string => $this->db->protectIdentifiers($field), array_values(array_unique($keyFields)));
// Get the binds
$binds = $this->binds;
@@ -596,7 +597,7 @@ protected function compileSelect($selectOverride = false): string
$sql = $this->_limit($sql . "\n");
}
- return $sql;
+ return $this->unionInjection($sql);
}
/**
diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php
index 3c86839d2e88..f180ca4f6b3b 100755
--- a/system/Database/SQLSRV/Connection.php
+++ b/system/Database/SQLSRV/Connection.php
@@ -120,6 +120,10 @@ public function connect(bool $persistent = false)
unset($connection['UID'], $connection['PWD']);
}
+ if (strpos($this->hostname, ',') === false && $this->port !== '') {
+ $this->hostname .= ', ' . $this->port;
+ }
+
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connID = sqlsrv_connect($this->hostname, $connection);
@@ -229,9 +233,7 @@ protected function _indexData(string $table): array
$obj->name = $row->index_name;
$_fields = explode(',', trim($row->index_keys));
- $obj->fields = array_map(static function ($v) {
- return trim($v);
- }, $_fields);
+ $obj->fields = array_map(static fn ($v) => trim($v), $_fields);
if (strpos($row->index_description, 'primary key located on') !== false) {
$obj->type = 'PRIMARY';
@@ -467,6 +469,8 @@ protected function execute(string $sql)
* Returns the last error encountered by this connection.
*
* @return mixed
+ *
+ * @deprecated Use `error()` instead.
*/
public function getError()
{
diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php
index 14d297604cbe..67ef057e12de 100755
--- a/system/Database/SQLSRV/Forge.php
+++ b/system/Database/SQLSRV/Forge.php
@@ -152,9 +152,7 @@ protected function _alterTable(string $alterType, string $table, $field)
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP ';
- $fields = array_map(static function ($item) {
- return 'COLUMN [' . trim($item) . ']';
- }, (array) $field);
+ $fields = array_map(static fn ($item) => 'COLUMN [' . trim($item) . ']', (array) $field);
return $sql . implode(',', $fields);
}
diff --git a/system/Database/SQLSRV/Utils.php b/system/Database/SQLSRV/Utils.php
index cf94d3dad783..22a12bcdf02a 100755
--- a/system/Database/SQLSRV/Utils.php
+++ b/system/Database/SQLSRV/Utils.php
@@ -34,7 +34,7 @@ class Utils extends BaseUtils
*/
protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE';
- public function __construct(ConnectionInterface &$db)
+ public function __construct(ConnectionInterface $db)
{
parent::__construct($db);
diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php
index 39fbca8bcfea..35ede04c2c9c 100644
--- a/system/Database/SQLite3/Connection.php
+++ b/system/Database/SQLite3/Connection.php
@@ -37,6 +37,20 @@ class Connection extends BaseConnection
*/
public $escapeChar = '`';
+ /**
+ * @var bool Enable Foreign Key constraint or not
+ */
+ protected $foreignKeys = false;
+
+ public function initialize()
+ {
+ parent::initialize();
+
+ if ($this->foreignKeys) {
+ $this->enableForeignKeyChecks();
+ }
+ }
+
/**
* Connect to the database.
*
diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php
index 8f04c0f645f1..1ecb28fdf8b7 100644
--- a/system/Database/SQLite3/Table.php
+++ b/system/Database/SQLite3/Table.php
@@ -28,6 +28,7 @@ class Table
* All of the fields this table represents.
*
* @var array
+ * @phpstan-var array>
*/
protected $fields = [];
@@ -276,10 +277,18 @@ protected function copyData()
$exFields[] = $name;
}
- $exFields = implode(', ', $exFields);
- $newFields = implode(', ', $newFields);
-
- $this->db->query("INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}");
+ $exFields = implode(
+ ', ',
+ array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields)
+ );
+ $newFields = implode(
+ ', ',
+ array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields)
+ );
+
+ $this->db->query(
+ "INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}"
+ );
}
/**
@@ -289,6 +298,7 @@ protected function copyData()
* @param array|bool $fields
*
* @return mixed
+ * @phpstan-return ($fields is array ? array : mixed)
*/
protected function formatFields($fields)
{
diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php
index 92d281965c7e..893de7d56ec4 100644
--- a/system/Database/Seeder.php
+++ b/system/Database/Seeder.php
@@ -67,11 +67,9 @@ class Seeder
/**
* Faker Generator instance.
*
- * @var Generator|null
- *
* @deprecated
*/
- private static $faker;
+ private static ?Generator $faker = null;
/**
* Seeder constructor.
@@ -92,9 +90,9 @@ public function __construct(Database $config, ?BaseConnection $db = null)
$this->config = &$config;
- $db = $db ?? Database::connect($this->DBGroup);
+ $db ??= Database::connect($this->DBGroup);
- $this->db = &$db;
+ $this->db = $db;
$this->forge = Database::forge($this->DBGroup);
}
diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php
index d319c2f9746e..879dab718a16 100644
--- a/system/Debug/Exceptions.php
+++ b/system/Debug/Exceptions.php
@@ -102,14 +102,19 @@ public function exceptionHandler(Throwable $exception)
[$statusCode, $exitCode] = $this->determineCodes($exception);
if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) {
- log_message('critical', $exception->getMessage() . "\n{trace}", [
- 'trace' => $exception->getTraceAsString(),
+ log_message('critical', "{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()),
]);
}
if (! is_cli()) {
$this->response->setStatusCode($statusCode);
- header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode);
+ if (! headers_sent()) {
+ 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) {
$this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();
@@ -318,6 +323,8 @@ protected function determineCodes(Throwable $exception): array
/**
* This makes nicer looking paths for the error output.
+ *
+ * @deprecated Use dedicated `clean_path()` function.
*/
public static function cleanPath(string $file): string
{
@@ -352,11 +359,11 @@ public static function describeMemory(int $bytes): string
return $bytes . 'B';
}
- if ($bytes < 1048576) {
+ if ($bytes < 1_048_576) {
return round($bytes / 1024, 2) . 'KB';
}
- return round($bytes / 1048576, 2) . 'MB';
+ return round($bytes / 1_048_576, 2) . 'MB';
}
/**
@@ -430,4 +437,55 @@ public static function highlightFile(string $file, int $lineNumber, int $lines =
return '
+.. warning:: If an attacker injects a string like ``
+
+ // Becomes
+
+
+ // OR
+
+
Class Reference
===============
@@ -282,9 +255,9 @@ The methods provided by the parent class that are available are:
:rtype: int
Returns the currently status code for this response. If no status code has been set, a BadMethodCallException
- will be thrown::
+ will be thrown:
- echo $response->getStatusCode();
+ .. literalinclude:: response/014.php
.. php:method:: setStatusCode($code[, $reason=''])
@@ -293,23 +266,23 @@ The methods provided by the parent class that are available are:
:returns: The current Response instance
:rtype: ``CodeIgniter\HTTP\Response``
- Sets the HTTP status code that should be sent with this response::
+ Sets the HTTP status code that should be sent with this response:
- $response->setStatusCode(404);
+ .. literalinclude:: response/015.php
The reason phrase will be automatically generated based upon the official lists. If you need to set your own
- for a custom status code, you can pass the reason phrase as the second parameter::
+ for a custom status code, you can pass the reason phrase as the second parameter:
- $response->setStatusCode(230, "Tardis initiated");
+ .. literalinclude:: response/016.php
.. php:method:: getReasonPhrase()
:returns: The current reason phrase.
:rtype: string
- Returns the current status code for this response. If not status has been set, will return an empty string::
+ Returns the current status code for this response. If not status has been set, will return an empty string:
- echo $response->getReasonPhrase();
+ .. literalinclude:: response/017.php
.. php:method:: setDate($date)
@@ -317,10 +290,9 @@ The methods provided by the parent class that are available are:
:returns: The current response instance.
:rtype: ``CodeIgniter\HTTP\Response``
- Sets the date used for this response. The ``$date`` argument must be an instance of ``DateTime``::
+ Sets the date used for this response. The ``$date`` argument must be an instance of ``DateTime``:
- $date = DateTime::createFromFormat('j-M-Y', '15-Feb-2016');
- $response->setDate($date);
+ .. literalinclude:: response/018.php
.. php:method:: setContentType($mime[, $charset='UTF-8'])
@@ -329,16 +301,14 @@ The methods provided by the parent class that are available are:
:returns: The current response instance.
:rtype: ``CodeIgniter\HTTP\Response``
- Sets the content type this response represents::
+ Sets the content type this response represents:
- $response->setContentType('text/plain');
- $response->setContentType('text/html');
- $response->setContentType('application/json');
+ .. literalinclude:: response/019.php
By default, the method sets the character set to ``UTF-8``. If you need to change this, you can
- pass the character set as the second parameter::
+ pass the character set as the second parameter:
- $response->setContentType('text/plain', 'x-pig-latin');
+ .. literalinclude:: response/020.php
.. php:method:: noCache()
@@ -346,12 +316,9 @@ The methods provided by the parent class that are available are:
:rtype: ``CodeIgniter\HTTP\Response``
Sets the ``Cache-Control`` header to turn off all HTTP caching. This is the default setting
- of all response messages::
-
- $response->noCache();
+ of all response messages:
- // Sets the following header:
- Cache-Control: no-store, max-age=0, no-cache
+ .. literalinclude:: response/021.php
.. php:method:: setCache($options)
@@ -380,10 +347,9 @@ The methods provided by the parent class that are available are:
:rtype: ``CodeIgniter\HTTP\Response``
Sets the ``Last-Modified`` header. The ``$date`` object can be either a string or a ``DateTime``
- instance::
+ instance:
- $response->setLastModified(date('D, d M Y H:i:s'));
- $response->setLastModified(DateTime::createFromFormat('u', $time));
+ .. literalinclude:: response/022.php
.. php:method:: send(): Response
@@ -404,7 +370,7 @@ The methods provided by the parent class that are available are:
:param string $prefix: Cookie name prefix
:param bool $secure: Whether to only transfer the cookie through HTTPS
:param bool $httponly: Whether to only make the cookie accessible for HTTP requests (no JavaScript)
- :param string $samesite: The value for the SameSite cookie parameter. If set to ``''``, no SameSite attribute will be set on the cookie. If set to `null`, the default value from `config/App.php` will be used
+ :param string $samesite: The value for the SameSite cookie parameter. If set to ``''``, no SameSite attribute will be set on the cookie. If set to ``null``, the default value from **app/Config/Cookie.php** will be used
:rtype: void
Sets a cookie containing the values you specify. There are two ways to
@@ -414,54 +380,43 @@ The methods provided by the parent class that are available are:
**Array Method**
Using this method, an associative array is passed as the first
- parameter::
-
- $cookie = [
- 'name' => 'The Cookie Name',
- 'value' => 'The Value',
- 'expire' => '86500',
- 'domain' => '.some-domain.com',
- 'path' => '/',
- 'prefix' => 'myprefix_',
- 'secure' => true,
- 'httponly' => false,
- 'samesite' => 'Lax'
- ];
-
- $response->setCookie($cookie);
+ parameter:
- **Notes**
+ .. literalinclude:: response/023.php
- Only the name and value are required. To delete a cookie set it with the
- expiration blank.
+ Only the ``name`` and ``value`` are required. To delete a cookie set it with the
+ ``expire`` blank.
- The expiration is set in **seconds**, which will be added to the current
+ The ``expire`` is set in **seconds**, which will be added to the current
time. Do not include the time, but rather only the number of seconds
- from *now* that you wish the cookie to be valid. If the expiration is
+ from *now* that you wish the cookie to be valid. If the ``expire`` is
set to zero the cookie will only last as long as the browser is open.
+ .. note:: But if the ``value`` is set to empty string and the ``expire`` is set to ``0``,
+ the cookie will be deleted.
+
For site-wide cookies regardless of how your site is requested, add your
- URL to the **domain** starting with a period, like this:
+ URL to the ``domain`` starting with a period, like this:
.your-domain.com
- The path is usually not needed since the method sets a root path.
+ The ``path`` is usually not needed since the method sets a root path.
- The prefix is only needed if you need to avoid name collisions with
+ The ``prefix`` is only needed if you need to avoid name collisions with
other identically named cookies for your server.
- The secure flag is only needed if you want to make it a secure cookie
+ The ``secure`` flag is only needed if you want to make it a secure cookie
by setting it to ``true``.
- The SameSite value controls how cookies are shared between domains and sub-domains.
- Allowed values are 'None', 'Lax', 'Strict' or a blank string ``''``.
+ The ``samesite`` value controls how cookies are shared between domains and sub-domains.
+ Allowed values are ``'None'``, ``'Lax'``, ``'Strict'`` or a blank string ``''``.
If set to blank string, default SameSite attribute will be set.
**Discrete Parameters**
If you prefer, you can set the cookie by passing data using individual
- parameters::
+ parameters:
- $response->setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly, $samesite);
+ .. literalinclude:: response/024.php
.. php:method:: deleteCookie($name = ''[, $domain = ''[, $path = '/'[, $prefix = '']]])
@@ -471,25 +426,23 @@ The methods provided by the parent class that are available are:
:param string $prefix: Cookie name prefix
:rtype: void
- Delete an existing cookie by setting its expiry to ``0``.
-
- **Notes**
+ Delete an existing cookie.
- Only the name is required.
+ Only the ``name`` is required.
- The prefix is only needed if you need to avoid name collisions with
+ The ``prefix`` is only needed if you need to avoid name collisions with
other identically named cookies for your server.
- Provide a prefix if cookies should only be deleted for that subset.
- Provide a domain name if cookies should only be deleted for that domain.
- Provide a path name if cookies should only be deleted for that path.
+ Provide a ``prefix`` if cookies should only be deleted for that subset.
+ Provide a ``domain`` name if cookies should only be deleted for that domain.
+ Provide a ``path`` name if cookies should only be deleted for that path.
If any of the optional parameters are empty, then the same-named
cookie will be deleted across all that apply.
- Example::
+ Example:
- $response->deleteCookie($name);
+ .. literalinclude:: response/025.php
.. php:method:: hasCookie($name = ''[, $value = null[, $prefix = '']])
@@ -502,15 +455,15 @@ The methods provided by the parent class that are available are:
**Notes**
- Only the name is required. If a prefix is specified, it will be prepended to the cookie name.
+ Only the ``name`` is required. If a ``prefix`` is specified, it will be prepended to the cookie name.
- If no value is given, the method just checks for the existence of the named cookie.
- If a value is given, then the method checks that the cookie exists, and that it
+ If no ``value`` is given, the method just checks for the existence of the named cookie.
+ If a ``value`` is given, then the method checks that the cookie exists, and that it
has the prescribed value.
- Example::
+ Example:
- if ($response->hasCookie($name)) ...
+ .. literalinclude:: response/026.php
.. php:method:: getCookie($name = ''[, $prefix = ''])
@@ -519,11 +472,11 @@ The methods provided by the parent class that are available are:
:rtype: ``Cookie|Cookie[]|null``
Returns the named cookie, if found, or ``null``.
- If no name is given, returns the array of ``Cookie`` objects.
+ If no ``name`` is given, returns the array of ``Cookie`` objects.
- Example::
+ Example:
- $cookie = $response->getCookie($name);
+ .. literalinclude:: response/027.php
.. php:method:: getCookies()
diff --git a/user_guide_src/source/outgoing/response/001.php b/user_guide_src/source/outgoing/response/001.php
new file mode 100644
index 000000000000..9815f6f1475d
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/001.php
@@ -0,0 +1,3 @@
+response->setStatusCode(404)->setBody($body);
diff --git a/user_guide_src/source/outgoing/response/002.php b/user_guide_src/source/outgoing/response/002.php
new file mode 100644
index 000000000000..d5ed0fd87d96
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/002.php
@@ -0,0 +1,3 @@
+response->setStatusCode(404, 'Nope. Not here.');
diff --git a/user_guide_src/source/outgoing/response/003.php b/user_guide_src/source/outgoing/response/003.php
new file mode 100644
index 000000000000..e5bff8cabf2f
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/003.php
@@ -0,0 +1,10 @@
+ true,
+ 'id' => 123,
+];
+
+return $this->response->setJSON($data);
+// or
+return $this->response->setXML($data);
diff --git a/user_guide_src/source/outgoing/response/004.php b/user_guide_src/source/outgoing/response/004.php
new file mode 100644
index 000000000000..0497dea06e58
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/004.php
@@ -0,0 +1,4 @@
+setHeader('Location', 'http://example.com')
+ ->setHeader('WWW-Authenticate', 'Negotiate');
diff --git a/user_guide_src/source/outgoing/response/005.php b/user_guide_src/source/outgoing/response/005.php
new file mode 100644
index 000000000000..39276d3f80f1
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/005.php
@@ -0,0 +1,4 @@
+setHeader('Cache-Control', 'no-cache')
+ ->appendHeader('Cache-Control', 'must-revalidate');
diff --git a/user_guide_src/source/outgoing/response/006.php b/user_guide_src/source/outgoing/response/006.php
new file mode 100644
index 000000000000..a2fdee1e9ed5
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/006.php
@@ -0,0 +1,3 @@
+removeHeader('Location');
diff --git a/user_guide_src/source/outgoing/response/007.php b/user_guide_src/source/outgoing/response/007.php
new file mode 100644
index 000000000000..9f80218ee669
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/007.php
@@ -0,0 +1,6 @@
+download($name, $data);
diff --git a/user_guide_src/source/outgoing/response/008.php b/user_guide_src/source/outgoing/response/008.php
new file mode 100644
index 000000000000..832d75fc0616
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/008.php
@@ -0,0 +1,4 @@
+download('/path/to/photo.jpg', null);
diff --git a/user_guide_src/source/outgoing/response/009.php b/user_guide_src/source/outgoing/response/009.php
new file mode 100644
index 000000000000..e75c386647cb
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/009.php
@@ -0,0 +1,3 @@
+download('awkwardEncryptedFileName.fakeExt', null)->setFileName('expenses.csv');
diff --git a/user_guide_src/source/outgoing/response/010.php b/user_guide_src/source/outgoing/response/010.php
new file mode 100644
index 000000000000..93acf62e99fa
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/010.php
@@ -0,0 +1,8 @@
+ 300,
+ 's-maxage' => 900,
+ 'etag' => 'abcde',
+];
+$this->response->setCache($options);
diff --git a/user_guide_src/source/outgoing/response/011.php b/user_guide_src/source/outgoing/response/011.php
new file mode 100644
index 000000000000..4a21b5c29078
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/011.php
@@ -0,0 +1,12 @@
+CSP->reportOnly(false);
+
+// specify the origin to use if none provided for a directive
+$response->CSP->setDefaultSrc('cdn.example.com');
+
+// specify the URL that "report-only" reports get sent to
+$response->CSP->setReportURI('http://example.com/csp/reports');
+
+// specify that HTTP requests be upgraded to HTTPS
+$response->CSP->upgradeInsecureRequests(true);
+
+// add types or origins to CSP directives
+// assuming that the default treatment is to block rather than just report
+$response->CSP->addBaseURI('example.com', true); // report only
+$response->CSP->addChildSrc('https://youtube.com'); // blocked
+$response->CSP->addConnectSrc('https://*.facebook.com', false); // blocked
+$response->CSP->addFontSrc('fonts.example.com');
+$response->CSP->addFormAction('self');
+$response->CSP->addFrameAncestor('none', true); // report this one
+$response->CSP->addImageSrc('cdn.example.com');
+$response->CSP->addMediaSrc('cdn.example.com');
+$response->CSP->addManifestSrc('cdn.example.com');
+$response->CSP->addObjectSrc('cdn.example.com', false); // reject from here
+$response->CSP->addPluginType('application/pdf', false); // reject this media type
+$response->CSP->addScriptSrc('scripts.example.com', true); // allow but report requests from here
+$response->CSP->addStyleSrc('css.example.com');
+$response->CSP->addSandbox(['allow-forms', 'allow-scripts']);
diff --git a/user_guide_src/source/outgoing/response/013.php b/user_guide_src/source/outgoing/response/013.php
new file mode 100644
index 000000000000..273d72c29a1d
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/013.php
@@ -0,0 +1,6 @@
+addChildSrc('https://youtube.com'); // allowed
+$response->reportOnly(true);
+$response->addChildSrc('https://metube.com'); // allowed but reported
+$response->addChildSrc('https://ourtube.com', false); // allowed
diff --git a/user_guide_src/source/outgoing/response/014.php b/user_guide_src/source/outgoing/response/014.php
new file mode 100644
index 000000000000..c05ef6fbe606
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/014.php
@@ -0,0 +1,3 @@
+getStatusCode();
diff --git a/user_guide_src/source/outgoing/response/015.php b/user_guide_src/source/outgoing/response/015.php
new file mode 100644
index 000000000000..3fbc71bc6371
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/015.php
@@ -0,0 +1,3 @@
+setStatusCode(404);
diff --git a/user_guide_src/source/outgoing/response/016.php b/user_guide_src/source/outgoing/response/016.php
new file mode 100644
index 000000000000..31a4990eb9ee
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/016.php
@@ -0,0 +1,3 @@
+setStatusCode(230, 'Tardis initiated');
diff --git a/user_guide_src/source/outgoing/response/017.php b/user_guide_src/source/outgoing/response/017.php
new file mode 100644
index 000000000000..78ce30be95bb
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/017.php
@@ -0,0 +1,3 @@
+getReasonPhrase();
diff --git a/user_guide_src/source/outgoing/response/018.php b/user_guide_src/source/outgoing/response/018.php
new file mode 100644
index 000000000000..606424af8f79
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/018.php
@@ -0,0 +1,4 @@
+setDate($date);
diff --git a/user_guide_src/source/outgoing/response/019.php b/user_guide_src/source/outgoing/response/019.php
new file mode 100644
index 000000000000..f74953ae772d
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/019.php
@@ -0,0 +1,5 @@
+setContentType('text/plain');
+$response->setContentType('text/html');
+$response->setContentType('application/json');
diff --git a/user_guide_src/source/outgoing/response/020.php b/user_guide_src/source/outgoing/response/020.php
new file mode 100644
index 000000000000..fdbc546b6fad
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/020.php
@@ -0,0 +1,3 @@
+setContentType('text/plain', 'x-pig-latin');
diff --git a/user_guide_src/source/outgoing/response/021.php b/user_guide_src/source/outgoing/response/021.php
new file mode 100644
index 000000000000..d5275cfb734e
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/021.php
@@ -0,0 +1,7 @@
+noCache();
+/*
+ * Sets the following header:
+ * Cache-Control: no-store, max-age=0, no-cache
+ */
diff --git a/user_guide_src/source/outgoing/response/022.php b/user_guide_src/source/outgoing/response/022.php
new file mode 100644
index 000000000000..5a841f28d2ce
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/022.php
@@ -0,0 +1,4 @@
+setLastModified(date('D, d M Y H:i:s'));
+$response->setLastModified(DateTime::createFromFormat('u', $time));
diff --git a/user_guide_src/source/outgoing/response/023.php b/user_guide_src/source/outgoing/response/023.php
new file mode 100644
index 000000000000..3d0855739e96
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/023.php
@@ -0,0 +1,15 @@
+ 'The Cookie Name',
+ 'value' => 'The Value',
+ 'expire' => '86500',
+ 'domain' => '.some-domain.com',
+ 'path' => '/',
+ 'prefix' => 'myprefix_',
+ 'secure' => true,
+ 'httponly' => false,
+ 'samesite' => 'Lax',
+];
+
+$response->setCookie($cookie);
diff --git a/user_guide_src/source/outgoing/response/024.php b/user_guide_src/source/outgoing/response/024.php
new file mode 100644
index 000000000000..3b75c4004362
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/024.php
@@ -0,0 +1,3 @@
+setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly, $samesite);
diff --git a/user_guide_src/source/outgoing/response/025.php b/user_guide_src/source/outgoing/response/025.php
new file mode 100644
index 000000000000..a2343928babe
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/025.php
@@ -0,0 +1,3 @@
+deleteCookie($name);
diff --git a/user_guide_src/source/outgoing/response/026.php b/user_guide_src/source/outgoing/response/026.php
new file mode 100644
index 000000000000..ee92fa38c094
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/026.php
@@ -0,0 +1,5 @@
+hasCookie($name)) {
+ // ...
+}
diff --git a/user_guide_src/source/outgoing/response/027.php b/user_guide_src/source/outgoing/response/027.php
new file mode 100644
index 000000000000..1d76679940c8
--- /dev/null
+++ b/user_guide_src/source/outgoing/response/027.php
@@ -0,0 +1,3 @@
+getCookie($name);
diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst
index 01b7dabe5cef..e40cf5d6ee57 100644
--- a/user_guide_src/source/outgoing/table.rst
+++ b/user_guide_src/source/outgoing/table.rst
@@ -17,9 +17,9 @@ Initializing the Class
======================
The Table class is not provided as a service, and should be instantiated
-"normally", for instance::
+"normally", for instance:
- $table = new \CodeIgniter\View\Table();
+.. literalinclude:: table/001.php
Examples
========
@@ -29,100 +29,32 @@ multi-dimensional array. Note that the first array index will become the
table heading (or you can set your own headings using the ``setHeading()``
method described in the function reference below).
-::
-
- $table = new \CodeIgniter\View\Table();
-
- $data = [
- ['Name', 'Color', 'Size'],
- ['Fred', 'Blue', 'Small'],
- ['Mary', 'Red', 'Large'],
- ['John', 'Green', 'Medium'],
- ];
-
- echo $table->generate($data);
+.. literalinclude:: table/002.php
Here is an example of a table created from a database query result. The
table class will automatically generate the headings based on the table
names (or you can set your own headings using the ``setHeading()``
method described in the class reference below).
-::
-
- $table = new \CodeIgniter\View\Table();
-
- $query = $db->query('SELECT * FROM my_table');
-
- echo $table->generate($query);
+.. literalinclude:: table/003.php
Here is an example showing how you might create a table using discrete
-parameters::
+parameters:
- $table = new \CodeIgniter\View\Table();
-
- $table->setHeading('Name', 'Color', 'Size');
-
- $table->addRow('Fred', 'Blue', 'Small');
- $table->addRow('Mary', 'Red', 'Large');
- $table->addRow('John', 'Green', 'Medium');
-
- echo $table->generate();
+.. literalinclude:: table/004.php
Here is the same example, except instead of individual parameters,
-arrays are used::
-
- $table = new \CodeIgniter\View\Table();
-
- $table->setHeading(array('Name', 'Color', 'Size'));
+arrays are used:
- $table->addRow(['Fred', 'Blue', 'Small']);
- $table->addRow(['Mary', 'Red', 'Large']);
- $table->addRow(['John', 'Green', 'Medium']);
-
- echo $table->generate();
+.. literalinclude:: table/005.php
Changing the Look of Your Table
===============================
The Table Class permits you to set a table template with which you can
-specify the design of your layout. Here is the template prototype::
-
- $template = [
- 'table_open' => '
'
- ];
+specify the design of your layout. Here is the template prototype:
- $table->setTemplate($template);
+.. literalinclude:: table/006.php
.. note:: You'll notice there are two sets of "row" blocks in the
template. These permit you to create alternating row colors or design
@@ -130,23 +62,14 @@ specify the design of your layout. Here is the template prototype::
You are NOT required to submit a complete template. If you only need to
change parts of the layout you can simply submit those elements. In this
-example, only the table opening tag is being changed::
+example, only the table opening tag is being changed:
- $template = [
- 'table_open' => '
'
- ];
-
- $table->setTemplate($template);
+.. literalinclude:: table/007.php
You can also set defaults for these by passing an array of template settings
-to the Table constructor.::
-
- $customSettings = [
- 'table_open' => '
'
- ];
-
- $table = new \CodeIgniter\View\Table($customSettings);
+to the Table constructor:
+.. literalinclude:: table/008.php
***************
Class Reference
@@ -157,15 +80,8 @@ Class Reference
.. attribute:: $function = null
Allows you to specify a native PHP function or a valid function array object to be applied to all cell data.
- ::
-
- $table = new \CodeIgniter\View\Table();
-
- $table->setHeading('Name', 'Color', 'Size');
- $table->addRow('Fred', 'Blue', 'Small');
- $table->function = 'htmlspecialchars';
- echo $table->generate();
+ .. literalinclude:: table/009.php
In the above example, all cell data would be run through PHP's :php:func:`htmlspecialchars()` function, resulting in::
@@ -186,9 +102,8 @@ Class Reference
:rtype: Table
Permits you to add a caption to the table.
- ::
- $table->setCaption('Colors');
+ .. literalinclude:: table/010.php
.. php:method:: setHeading([$args = [] [, ...]])
@@ -196,11 +111,9 @@ Class Reference
:returns: Table instance (method chaining)
:rtype: Table
- Permits you to set the table heading. You can submit an array or discrete params::
+ Permits you to set the table heading. You can submit an array or discrete params:
- $table->setHeading('Name', 'Color', 'Size'); // or
-
- $table->setHeading(['Name', 'Color', 'Size']);
+ .. literalinclude:: table/011.php
.. php:method:: setFooting([$args = [] [, ...]])
@@ -208,11 +121,9 @@ Class Reference
:returns: Table instance (method chaining)
:rtype: Table
- Permits you to set the table footing. You can submit an array or discrete params::
-
- $table->setFooting('Subtotal', $subtotal, $notes); // or
+ Permits you to set the table footing. You can submit an array or discrete params:
- $table->setFooting(['Subtotal', $subtotal, $notes]);
+ .. literalinclude:: table/012.php
.. php:method:: addRow([$args = [] [, ...]])
@@ -220,20 +131,14 @@ Class Reference
:returns: Table instance (method chaining)
:rtype: Table
- Permits you to add a row to your table. You can submit an array or discrete params::
-
- $table->addRow('Blue', 'Red', 'Green'); // or
+ Permits you to add a row to your table. You can submit an array or discrete params:
- $table->addRow(['Blue', 'Red', 'Green']);
+ .. literalinclude:: table/013.php
If you would like to set an individual cell's tag attributes, you can use an associative array for that cell.
- The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag::
+ The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag:
- $cell = ['data' => 'Blue', 'class' => 'highlight', 'colspan' => 2];
- $table->addRow($cell, 'Red', 'Green');
-
- // generates
- //
Blue
Red
Green
+ .. literalinclude:: table/014.php
.. php:method:: makeColumns([$array = [] [, $columnLimit = 0]])
@@ -243,27 +148,9 @@ Class Reference
:rtype: array
This method takes a one-dimensional array as input and creates a multi-dimensional array with a depth equal to the number of columns desired.
- This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example::
-
- $list = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
-
- $newList = $table->makeColumns($list, 3);
-
- $table->generate($newList);
-
- // Generates a table with this prototype
-
-
-
-
one
two
three
-
-
four
five
six
-
-
seven
eight
nine
-
-
ten
eleven
twelve
-
+ This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example:
+ .. literalinclude:: table/015.php
.. php:method:: setTemplate($template)
@@ -272,13 +159,8 @@ Class Reference
:rtype: bool
Permits you to set your template. You can submit a full or partial template.
- ::
-
- $template = [
- 'table_open' => '
'
- ];
- $table->setTemplate($template);
+ .. literalinclude:: table/016.php
.. php:method:: setEmpty($value)
@@ -287,9 +169,9 @@ Class Reference
:rtype: Table
Lets you set a default value for use in any table cells that are empty.
- You might, for example, set a non-breaking space::
+ You might, for example, set a non-breaking space:
- $table->setEmpty(" ");
+ .. literalinclude:: table/017.php
.. php:method:: clear()
@@ -301,24 +183,6 @@ Class Reference
should to call this method after each table has been
generated to clear the previous table information.
- Example ::
-
- $table = new \CodeIgniter\View\Table();
-
- $table->setCaption('Preferences')
- ->setHeading('Name', 'Color', 'Size')
- ->addRow('Fred', 'Blue', 'Small')
- ->addRow('Mary', 'Red', 'Large')
- ->addRow('John', 'Green', 'Medium');
-
- echo $table->generate();
-
- $table->clear();
-
- $table->setCaption('Shipping')
- ->setHeading('Name', 'Day', 'Delivery')
- ->addRow('Fred', 'Wednesday', 'Express')
- ->addRow('Mary', 'Monday', 'Air')
- ->addRow('John', 'Saturday', 'Overnight');
+ Example
- echo $table->generate();
+ .. literalinclude:: table/018.php
diff --git a/user_guide_src/source/outgoing/table/001.php b/user_guide_src/source/outgoing/table/001.php
new file mode 100644
index 000000000000..dbd5ba323335
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/001.php
@@ -0,0 +1,3 @@
+generate($data);
diff --git a/user_guide_src/source/outgoing/table/003.php b/user_guide_src/source/outgoing/table/003.php
new file mode 100644
index 000000000000..91218de3f143
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/003.php
@@ -0,0 +1,7 @@
+query('SELECT * FROM my_table');
+
+echo $table->generate($query);
diff --git a/user_guide_src/source/outgoing/table/004.php b/user_guide_src/source/outgoing/table/004.php
new file mode 100644
index 000000000000..0d0679305409
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/004.php
@@ -0,0 +1,11 @@
+setHeading('Name', 'Color', 'Size');
+
+$table->addRow('Fred', 'Blue', 'Small');
+$table->addRow('Mary', 'Red', 'Large');
+$table->addRow('John', 'Green', 'Medium');
+
+echo $table->generate();
diff --git a/user_guide_src/source/outgoing/table/005.php b/user_guide_src/source/outgoing/table/005.php
new file mode 100644
index 000000000000..08d6dabb1850
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/005.php
@@ -0,0 +1,11 @@
+setHeading(['Name', 'Color', 'Size']);
+
+$table->addRow(['Fred', 'Blue', 'Small']);
+$table->addRow(['Mary', 'Red', 'Large']);
+$table->addRow(['John', 'Green', 'Medium']);
+
+echo $table->generate();
diff --git a/user_guide_src/source/outgoing/table/006.php b/user_guide_src/source/outgoing/table/006.php
new file mode 100644
index 000000000000..a5955aa5704b
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/006.php
@@ -0,0 +1,38 @@
+ '
diff --git a/user_guide_src/source/outgoing/table/016.php b/user_guide_src/source/outgoing/table/016.php
new file mode 100644
index 000000000000..022abfc296ad
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/016.php
@@ -0,0 +1,7 @@
+ '
',
+];
+
+$table->setTemplate($template);
diff --git a/user_guide_src/source/outgoing/table/017.php b/user_guide_src/source/outgoing/table/017.php
new file mode 100644
index 000000000000..8f7b203a3344
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/017.php
@@ -0,0 +1,3 @@
+setEmpty(' ');
diff --git a/user_guide_src/source/outgoing/table/018.php b/user_guide_src/source/outgoing/table/018.php
new file mode 100644
index 000000000000..c67c746bde9b
--- /dev/null
+++ b/user_guide_src/source/outgoing/table/018.php
@@ -0,0 +1,21 @@
+setCaption('Preferences')
+ ->setHeading('Name', 'Color', 'Size')
+ ->addRow('Fred', 'Blue', 'Small')
+ ->addRow('Mary', 'Red', 'Large')
+ ->addRow('John', 'Green', 'Medium');
+
+echo $table->generate();
+
+$table->clear();
+
+$table->setCaption('Shipping')
+ ->setHeading('Name', 'Day', 'Delivery')
+ ->addRow('Fred', 'Wednesday', 'Express')
+ ->addRow('Mary', 'Monday', 'Air')
+ ->addRow('John', 'Saturday', 'Overnight');
+
+echo $table->generate();
diff --git a/user_guide_src/source/outgoing/view_decorators.rst b/user_guide_src/source/outgoing/view_decorators.rst
new file mode 100644
index 000000000000..83f3154f54c5
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_decorators.rst
@@ -0,0 +1,23 @@
+###############
+View Decorators
+###############
+
+View Decorators allow your application to modify the HTML output during the rendering process. This happens just
+prior to being cached, and allows you to apply custom functionality to your views.
+
+*******************
+Creating Decorators
+*******************
+
+Creating your own view decorators requires creating a new class that implements ``CodeIgniter\Views\ViewDecoratorInterface``.
+This requires a single method that takes the generated HTML string, performs any modifications on it, and returns
+the resulting HTML.
+
+.. literalinclude:: view_decorators/001.php
+
+Once created, the class must be registered in ``app/Config/View.php``:
+
+.. literalinclude:: view_decorators/002.php
+
+Now that it's registered the decorator will be called for every view that is rendered or parsed.
+Decorators are called in the order specified in this configuration setting.
diff --git a/user_guide_src/source/outgoing/view_decorators/001.php b/user_guide_src/source/outgoing/view_decorators/001.php
new file mode 100644
index 000000000000..be05e93b5818
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_decorators/001.php
@@ -0,0 +1,15 @@
+endSection() ?>
= $this->endSection() ?>
-
******************
Rendering the View
******************
-Rendering the view and it's layout is done exactly as any other view would be displayed within a controller::
+Rendering the view and it's layout is done exactly as any other view would be displayed within a controller:
- public function index()
- {
- echo view('some_view');
- }
+.. literalinclude:: view_layouts/001.php
It renders the View **app/Views/some_view.php** and if it extends ``default``,
the Layout **app/Views/default.php** is also used automatically.
diff --git a/user_guide_src/source/outgoing/view_layouts/001.php b/user_guide_src/source/outgoing/view_layouts/001.php
new file mode 100644
index 000000000000..f6105365fa47
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_layouts/001.php
@@ -0,0 +1,11 @@
+ 'My Blog Title',
- 'blog_heading' => 'My Blog Heading',
- ];
-
- echo $parser->setData($data)
- ->render('blog_template');
+.. literalinclude:: view_parser/003.php
View parameters are passed to ``setData()`` as an associative
array of data to be replaced in the template. In the above example, the
@@ -112,16 +106,11 @@ Several options can be passed to the ``render()`` or ``renderString()`` methods.
- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath;
ignored for renderString()
- ``saveData`` - true if the view data parameters should be retained for subsequent calls;
- default is **false**
+ default is **true**
- ``cascadeData`` - true if pseudo-variable settings should be passed on to nested
substitutions; default is **true**
-::
-
- echo $parser->render('blog_template', [
- 'cache' => HOUR,
- 'cache_name' => 'something_unique',
- ]);
+.. literalinclude:: view_parser/004.php
***********************
Substitution Variations
@@ -132,14 +121,9 @@ Substitutions are performed in the same sequence that pseudo-variables were adde
The **simple substitution** performed by the parser is a one-to-one
replacement of pseudo-variables where the corresponding data parameter
-has either a scalar or string value, as in this example::
+has either a scalar or string value, as in this example:
- $template = '{blog_title}';
- $data = ['blog_title' => 'My ramblings'];
-
- echo $parser->setData($data)->renderString($template);
-
- // Result: My ramblings
+.. literalinclude:: view_parser/005.php
The ``Parser`` takes substitution a lot further with "variable pairs",
used for nested substitutions or looping, and with some advanced
@@ -184,22 +168,9 @@ the number of rows in the "blog_entries" element of the parameters array.
Parsing variable pairs is done using the identical code shown above to
parse single variables, except, you will add a multi-dimensional array
-corresponding to your variable pair data. Consider this example::
-
- $data = [
- 'blog_title' => 'My Blog Title',
- 'blog_heading' => 'My Blog Heading',
- 'blog_entries' => [
- ['title' => 'Title 1', 'body' => 'Body 1'],
- ['title' => 'Title 2', 'body' => 'Body 2'],
- ['title' => 'Title 3', 'body' => 'Body 3'],
- ['title' => 'Title 4', 'body' => 'Body 4'],
- ['title' => 'Title 5', 'body' => 'Body 5'],
- ],
- ];
+corresponding to your variable pair data. Consider this example:
- echo $parser->setData($data)
- ->render('blog_template');
+.. literalinclude:: view_parser/006.php
The value for the pseudo-variable ``blog_entries`` is a sequential
array of associative arrays. The outer level does not have keys associated
@@ -207,18 +178,9 @@ with each of the nested "rows".
If your "pair" data is coming from a database result, which is already a
multi-dimensional array, you can simply use the database ``getResultArray()``
-method::
-
- $query = $db->query("SELECT * FROM blog");
-
- $data = [
- 'blog_title' => 'My Blog Title',
- 'blog_heading' => 'My Blog Heading',
- 'blog_entries' => $query->getResultArray(),
- ];
+method:
- echo $parser->setData($data)
- ->render('blog_template');
+.. literalinclude:: view_parser/007.php
If the array you are trying to loop over contains objects instead of arrays,
the parser will first look for an ``asArray()`` method on the object. If it exists,
@@ -234,19 +196,9 @@ Nested Substitutions
====================
A nested substitution happens when the value for a pseudo-variable is
-an associative array of values, like a record from a database::
+an associative array of values, like a record from a database:
- $data = [
- 'blog_title' => 'My Blog Title',
- 'blog_heading' => 'My Blog Heading',
- 'blog_entry' => [
- 'title' => 'Title 1',
- 'body' => 'Body 1',
- ],
- ];
-
- echo $parser->setData($data)
- ->render('blog_template');
+.. literalinclude:: view_parser/008.php
The value for the pseudo-variable ``blog_entry`` is an associative
array. The key/value pairs defined inside it will be exposed inside
@@ -287,37 +239,18 @@ Cascading Data
With both a nested and a loop substitution, you have the option of cascading
data pairs into the inner substitution.
-The following example is not impacted by cascading::
+The following example is not impacted by cascading:
- $template = '{name} lives in {location}{city} on {planet}{/location}.';
+.. literalinclude:: view_parser/009.php
- $data = [
- 'name' => 'George',
- 'location' => ['city' => 'Red City', 'planet' => 'Mars'],
- ];
-
- echo $parser->setData($data)->renderString($template);
- // Result: George lives in Red City on Mars.
-
-This example gives different results, depending on cascading::
+This example gives different results, depending on cascading:
- $template = '{location}{name} lives in {city} on {planet}{/location}.';
-
- $data = [
- 'name' => 'George',
- 'location' => ['city' => 'Red City', 'planet' => 'Mars'],
- ];
-
- echo $parser->setData($data)->renderString($template, ['cascadeData'=>false]);
- // Result: {name} lives in Red City on Mars.
-
- echo $parser->setData($data)->renderString($template, ['cascadeData'=>true]);
- // Result: George lives in Red City on Mars.
+.. literalinclude:: view_parser/010.php
Preventing Parsing
==================
-You can specify portions of the page to not be parsed with the ``{noparse}{/noparse}`` tag pair. Anything in this
+You can specify portions of the page to not be parsed with the ``{noparse}`` ``{/noparse}`` tag pair. Anything in this
section will stay exactly as it is, with no variable substitution, looping, etc, happening to the markup between the brackets.
::
@@ -336,11 +269,9 @@ blocks must be closed with an ``endif`` tag::
Welcome, Admin!
{endif}
-This simple block is converted to the following during parsing::
+This simple block is converted to the following during parsing:
-
-
Welcome, Admin!
-
+.. literalinclude:: view_parser/011.php
All variables used within if statements must have been previously set with the same name. Other than that, it is
treated exactly like a standard PHP conditional, and all standard PHP rules would apply here. You can use any
@@ -359,6 +290,31 @@ of the comparison operators you would normally, like ``==``, ``===``, ``!==``, `
.. warning:: In the background, conditionals are parsed using an ``eval()``, so you must ensure that you take
care with the user data that is used within conditionals, or you could open your application up to security risks.
+Changing the Conditional Delimiters
+-----------------------------------
+
+If you have JavaScript code like the following in your templates, the Parser raises a syntax error because there are strings that can be interpreted as a conditional::
+
+
+
+In that case, you can change the delimiters for conditionals with the ``setConditionalDelimiters()`` method to avoid misinterpretations:
+
+.. literalinclude:: view_parser/027.php
+
+In this case, you will write code in your template::
+
+ {% if $role=='admin' %}
+
Welcome, Admin
+ {% else %}
+
Welcome, User
+ {% endif %}
+
Escaping Data
=============
@@ -380,7 +336,7 @@ Filters
Any single variable substitution can have one or more filters applied to it to modify the way it is presented. These
are not intended to drastically change the output, but provide ways to reuse the same variable data but with different
-presentations. The **esc** filter discussed above is one example. Dates are another common use case, where you might
+presentations. The ``esc`` filter discussed above is one example. Dates are another common use case, where you might
need to format the same data differently in several sections on the same page.
Filters are commands that come after the pseudo-variable name, and are separated by the pipe symbol, ``|``::
@@ -470,23 +426,18 @@ Custom Filters
You can easily create your own filters by editing **app/Config/View.php** and adding new entries to the
``$filters`` array. Each key is the name of the filter is called by in the view, and its value is any valid PHP
-callable::
+callable:
- public $filters = [
- 'abs' => '\CodeIgniter\View\Filters::abs',
- 'capitalize' => '\CodeIgniter\View\Filters::capitalize',
- ];
+.. literalinclude:: view_parser/012.php
PHP Native functions as Filters
-------------------------------
You can use native php function as filters by editing **app/Config/View.php** and adding new entries to the
``$filters`` array.Each key is the name of the native PHP function is called by in the view, and its value is any valid native PHP
-function prefixed with::
+function prefixed with:
- public $filters = [
- 'str_repeat' => '\str_repeat',
- ];
+.. literalinclude:: view_parser/013.php
Parser Plugins
==============
@@ -507,7 +458,7 @@ While plugins will often consist of tag pairs, like shown above, they can also b
Opening tags can also contain parameters that can customize how the plugin works. The parameters are represented as
key/value pairs::
- {+ foo bar=2 baz="x y" }
+ {+ foo bar=2 baz="x y" +}
Parameters can also be single values::
@@ -530,107 +481,54 @@ lang language string Alias for the lang helper function.
validation_errors fieldname(optional) Returns either error string for the field {+ validation_errors +} , {+ validation_errors field="email" +}
(if specified) or all validation errors.
route route name Alias for the route_to helper function. {+ route "login" +}
+csp_script_nonce Alias for the csp_script_nonce helper {+ csp_script_nonce +}
+ function.
+csp_style_nonce Alias for the csp_style_nonce helper {+ csp_style_nonce +}
+ function.
================== ========================= ============================================ ================================================================
Registering a Plugin
--------------------
At its simplest, all you need to do to register a new plugin and make it ready for use is to add it to the
-**app/Config/View.php**, under the **$plugins** array. The key is the name of the plugin that is
-used within the template file. The value is any valid PHP callable, including static class methods, and closures::
-
- public $plugins = [
- 'foo' => '\Some\Class::methodName',
- 'bar' => function ($str, array $params=[]) {
- return $str;
- },
- ];
+**app/Config/View.php**, under the ``$plugins`` array. The key is the name of the plugin that is
+used within the template file. The value is any valid PHP callable, including static class methods:
-Any closures that are being used must be defined in the config file's constructor::
+.. literalinclude:: view_parser/014.php
- class View extends \CodeIgniter\Config\View
- {
- public $plugins = [];
+You can also use closures, but these can only be defined in the config file's constructor:
- public function __construct()
- {
- $this->plugins['bar'] = function (array $params=[]) {
- return $params[0] ?? '';
- };
-
- parent::__construct();
- }
- }
+.. literalinclude:: view_parser/015.php
If the callable is on its own, it is treated as a single tag, not a open/close one. It will be replaced by
-the return value from the plugin::
+the return value from the plugin:
- public $plugins = [
- 'foo' => '\Some\Class::methodName'
- ];
-
- // Tag is replaced by the return value of Some\Class::methodName static function.
- {+ foo +}
+.. literalinclude:: view_parser/016.php
If the callable is wrapped in an array, it is treated as an open/close tag pair that can operate on any of
-the content between its tags::
-
- public $plugins = [
- 'foo' => ['\Some\Class::methodName']
- ];
+the content between its tags:
- {+ foo +} inner content {+ /foo +}
+.. literalinclude:: view_parser/017.php
***********
Usage Notes
***********
If you include substitution parameters that are not referenced in your
-template, they are ignored::
+template, they are ignored:
- $template = 'Hello, {firstname} {lastname}';
- $data = [
- 'title' => 'Mr',
- 'firstname' => 'John',
- 'lastname' => 'Doe'
- ];
- echo $parser->setData($data)
- ->renderString($template);
-
- // Result: Hello, John Doe
+.. literalinclude:: view_parser/018.php
If you do not include a substitution parameter that is referenced in your
-template, the original pseudo-variable is shown in the result::
-
- $template = 'Hello, {firstname} {initials} {lastname}';
- $data = [
- 'title' => 'Mr',
- 'firstname' => 'John',
- 'lastname' => 'Doe',
- ];
- echo $parser->setData($data)
- ->renderString($template);
+template, the original pseudo-variable is shown in the result:
- // Result: Hello, John {initials} Doe
+.. literalinclude:: view_parser/019.php
If you provide a string substitution parameter when an array is expected,
i.e., for a variable pair, the substitution is done for the opening variable
-pair tag, but the closing variable pair tag is not rendered properly::
-
- $template = 'Hello, {firstname} {lastname} ({degrees}{degree} {/degrees})';
- $data = [
- 'degrees' => 'Mr',
- 'firstname' => 'John',
- 'lastname' => 'Doe',
- 'titles' => [
- ['degree' => 'BSc'],
- ['degree' => 'PhD'],
- ],
- ];
- echo $parser->setData($data)
- ->renderString($template);
+pair tag, but the closing variable pair tag is not rendered properly:
- // Result: Hello, John Doe (Mr{degree} {/degrees})
+.. literalinclude:: view_parser/020.php
View Fragments
==============
@@ -652,8 +550,8 @@ An example with the iteration controlled in the view::
['title' => 'Second Link', 'link' => '/second'],
]
];
- echo $parser->setData($data)
- ->renderString($template);
+
+ return $parser->setData($data)->renderString($template);
Result::
@@ -663,25 +561,9 @@ Result::
An example with the iteration controlled in the controller,
-using a view fragment::
-
- $temp = '';
- $template1 = '
';
- $data = [
- 'menuitems' => $temp,
- ];
- echo $parser->setData($data)
- ->renderString($template2);
+.. literalinclude:: view_parser/021.php
Result::
@@ -696,7 +578,7 @@ Class Reference
.. php:class:: CodeIgniter\\View\\Parser
- .. php:method:: render($view[, $options[, $saveData = false]])
+ .. php:method:: render($view[, $options[, $saveData]])
:param string $view: File name of the view source
:param array $options: Array of options, as key/value pairs
@@ -704,9 +586,9 @@ Class Reference
:returns: The rendered text for the chosen view
:rtype: string
- Builds the output based upon a file name and any data that has already been set::
+ Builds the output based upon a file name and any data that has already been set:
- echo $parser->render('myview');
+ .. literalinclude:: view_parser/022.php
Options supported:
@@ -714,13 +596,11 @@ Class Reference
- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath
- ``cascadeData`` - true if the data pairs in effect when a nested or loop substitution occurs should be propagated
- ``saveData`` - true if the view data parameter should be retained for subsequent calls
- - ``leftDelimiter`` - the left delimiter to use in pseudo-variable syntax
- - ``rightDelimiter`` - the right delimiter to use in pseudo-variable syntax
Any conditional substitutions are performed first, then remaining
substitutions are performed for each data pair.
- .. php:method:: renderString($template[, $options[, $saveData = false]])
+ .. php:method:: renderString($template[, $options[, $saveData]])
:param string $template: View source provided as a string
:param array $options: Array of options, as key/value pairs
@@ -728,9 +608,9 @@ Class Reference
:returns: The rendered text for the chosen view
:rtype: string
- Builds the output based upon a provided template source and any data that has already been set::
+ Builds the output based upon a provided template source and any data that has already been set:
- echo $parser->render('myview');
+ .. literalinclude:: view_parser/023.php
Options supported, and behavior, as above.
@@ -741,9 +621,9 @@ Class Reference
:returns: The Renderer, for method chaining
:rtype: CodeIgniter\\View\\RendererInterface.
- Sets several pieces of view data at once::
+ Sets several pieces of view data at once:
- $renderer->setData(['name' => 'George', 'position' => 'Boss']);
+ .. literalinclude:: view_parser/024.php
Supported escape contexts: html, css, js, url, or attr or raw.
If 'raw', no escaping will happen.
@@ -756,9 +636,9 @@ Class Reference
:returns: The Renderer, for method chaining
:rtype: CodeIgniter\\View\\RendererInterface.
- Sets a single piece of view data::
+ Sets a single piece of view data:
- $renderer->setVar('name','Joe','html');
+ .. literalinclude:: view_parser/025.php
Supported escape contexts: html, css, js, url, attr or raw.
If 'raw', no escaping will happen.
@@ -770,6 +650,17 @@ Class Reference
:returns: The Renderer, for method chaining
:rtype: CodeIgniter\\View\\RendererInterface.
- Override the substitution field delimiters::
+ Override the substitution field delimiters:
+
+ .. literalinclude:: view_parser/026.php
+
+ .. php:method:: setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}')
+
+ :param string $leftDelimiter: Left delimiter for conditionals
+ :param string $rightDelimiter: right delimiter for conditionals
+ :returns: The Renderer, for method chaining
+ :rtype: CodeIgniter\\View\\RendererInterface.
+
+ Override the conditional delimiters:
- $renderer->setDelimiters('[',']');
+ .. literalinclude:: view_parser/027.php
diff --git a/user_guide_src/source/outgoing/view_parser/001.php b/user_guide_src/source/outgoing/view_parser/001.php
new file mode 100644
index 000000000000..56f7f51e4a07
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/001.php
@@ -0,0 +1,3 @@
+ 'My Blog Title',
+ 'blog_heading' => 'My Blog Heading',
+];
+
+return $parser->setData($data)->render('blog_template');
diff --git a/user_guide_src/source/outgoing/view_parser/004.php b/user_guide_src/source/outgoing/view_parser/004.php
new file mode 100644
index 000000000000..643f541a5b3e
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/004.php
@@ -0,0 +1,6 @@
+render('blog_template', [
+ 'cache' => HOUR,
+ 'cache_name' => 'something_unique',
+]);
diff --git a/user_guide_src/source/outgoing/view_parser/005.php b/user_guide_src/source/outgoing/view_parser/005.php
new file mode 100644
index 000000000000..e85e49db45a2
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/005.php
@@ -0,0 +1,7 @@
+{blog_title}';
+$data = ['blog_title' => 'My ramblings'];
+
+return $parser->setData($data)->renderString($template);
+// Result: My ramblings
diff --git a/user_guide_src/source/outgoing/view_parser/006.php b/user_guide_src/source/outgoing/view_parser/006.php
new file mode 100644
index 000000000000..40d379aec2ea
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/006.php
@@ -0,0 +1,15 @@
+ 'My Blog Title',
+ 'blog_heading' => 'My Blog Heading',
+ 'blog_entries' => [
+ ['title' => 'Title 1', 'body' => 'Body 1'],
+ ['title' => 'Title 2', 'body' => 'Body 2'],
+ ['title' => 'Title 3', 'body' => 'Body 3'],
+ ['title' => 'Title 4', 'body' => 'Body 4'],
+ ['title' => 'Title 5', 'body' => 'Body 5'],
+ ],
+];
+
+return $parser->setData($data)->render('blog_template');
diff --git a/user_guide_src/source/outgoing/view_parser/007.php b/user_guide_src/source/outgoing/view_parser/007.php
new file mode 100644
index 000000000000..5128ed3cbe4a
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/007.php
@@ -0,0 +1,11 @@
+query('SELECT * FROM blog');
+
+$data = [
+ 'blog_title' => 'My Blog Title',
+ 'blog_heading' => 'My Blog Heading',
+ 'blog_entries' => $query->getResultArray(),
+];
+
+return $parser->setData($data)->render('blog_template');
diff --git a/user_guide_src/source/outgoing/view_parser/008.php b/user_guide_src/source/outgoing/view_parser/008.php
new file mode 100644
index 000000000000..8cac9bbbaa9d
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/008.php
@@ -0,0 +1,12 @@
+ 'My Blog Title',
+ 'blog_heading' => 'My Blog Heading',
+ 'blog_entry' => [
+ 'title' => 'Title 1',
+ 'body' => 'Body 1',
+ ],
+];
+
+return $parser->setData($data)->render('blog_template');
diff --git a/user_guide_src/source/outgoing/view_parser/009.php b/user_guide_src/source/outgoing/view_parser/009.php
new file mode 100644
index 000000000000..6f4b96e82274
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/009.php
@@ -0,0 +1,11 @@
+ 'George',
+ 'location' => ['city' => 'Red City', 'planet' => 'Mars'],
+];
+
+return $parser->setData($data)->renderString($template);
+// Result: George lives in Red City on Mars.
diff --git a/user_guide_src/source/outgoing/view_parser/010.php b/user_guide_src/source/outgoing/view_parser/010.php
new file mode 100644
index 000000000000..4eb11fd6d908
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/010.php
@@ -0,0 +1,16 @@
+ 'George',
+ 'location' => ['city' => 'Red City', 'planet' => 'Mars'],
+];
+
+return $parser->setData($data)->renderString($template, ['cascadeData' => false]);
+// Result: {name} lives in Red City on Mars.
+
+// or
+
+return $parser->setData($data)->renderString($template, ['cascadeData' => true]);
+// Result: George lives in Red City on Mars.
diff --git a/user_guide_src/source/outgoing/view_parser/011.php b/user_guide_src/source/outgoing/view_parser/011.php
new file mode 100644
index 000000000000..88e2060b1ed5
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/011.php
@@ -0,0 +1,3 @@
+
+
';
+$data = [
+ 'menuitems' => $temp,
+];
+
+return $parser->setData($data)->renderString($template2);
diff --git a/user_guide_src/source/outgoing/view_parser/022.php b/user_guide_src/source/outgoing/view_parser/022.php
new file mode 100644
index 000000000000..1bdb256d92f1
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/022.php
@@ -0,0 +1,3 @@
+render('myview');
diff --git a/user_guide_src/source/outgoing/view_parser/023.php b/user_guide_src/source/outgoing/view_parser/023.php
new file mode 100644
index 000000000000..1bdb256d92f1
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/023.php
@@ -0,0 +1,3 @@
+render('myview');
diff --git a/user_guide_src/source/outgoing/view_parser/024.php b/user_guide_src/source/outgoing/view_parser/024.php
new file mode 100644
index 000000000000..17a0a477e756
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/024.php
@@ -0,0 +1,3 @@
+setData(['name' => 'George', 'position' => 'Boss']);
diff --git a/user_guide_src/source/outgoing/view_parser/025.php b/user_guide_src/source/outgoing/view_parser/025.php
new file mode 100644
index 000000000000..fe67fe17aeb4
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/025.php
@@ -0,0 +1,3 @@
+setVar('name', 'Joe', 'html');
diff --git a/user_guide_src/source/outgoing/view_parser/026.php b/user_guide_src/source/outgoing/view_parser/026.php
new file mode 100644
index 000000000000..a7efd14837f2
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/026.php
@@ -0,0 +1,3 @@
+setDelimiters('[', ']');
diff --git a/user_guide_src/source/outgoing/view_parser/027.php b/user_guide_src/source/outgoing/view_parser/027.php
new file mode 100644
index 000000000000..84730679ced5
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_parser/027.php
@@ -0,0 +1,3 @@
+setConditionalDelimiters('{%', '%}');
diff --git a/user_guide_src/source/outgoing/view_renderer.rst b/user_guide_src/source/outgoing/view_renderer.rst
index 62fc30f5920e..feda1affda3d 100644
--- a/user_guide_src/source/outgoing/view_renderer.rst
+++ b/user_guide_src/source/outgoing/view_renderer.rst
@@ -12,14 +12,14 @@ Using the View Renderer
The ``view()`` function is a convenience function that grabs an instance of the
``renderer`` service, sets the data, and renders the view. While this is often
exactly what you want, you may find times where you want to work with it more directly.
-In that case you can access the View service directly::
+In that case you can access the View service directly:
- $view = \Config\Services::renderer();
+.. literalinclude:: view_renderer/001.php
Alternately, if you are not using the ``View`` class as your default renderer, you
-can instantiate it directly::
+can instantiate it directly:
- $view = new \CodeIgniter\View\View();
+.. literalinclude:: view_renderer/002.php
.. important:: You should create services only within controllers. If you need
access to the View class from a library, you should set that as a dependency
@@ -48,12 +48,10 @@ to you to process the array appropriately in your PHP code.
Method Chaining
===============
-The `setVar()` and `setData()` methods are chainable, allowing you to combine a
-number of different calls together in a chain::
+The ``setVar()`` and ``setData()`` methods are chainable, allowing you to combine a
+number of different calls together in a chain:
- $view->setVar('one', $one)
- ->setVar('two', $two)
- ->render('myView');
+.. literalinclude:: view_renderer/003.php
Escaping Data
=============
@@ -62,9 +60,9 @@ When you pass data to the ``setVar()`` and ``setData()`` functions you have the
against cross-site scripting attacks. As the last parameter in either method, you can pass the desired context to
escape the data for. See below for context descriptions.
-If you don't want the data to be escaped, you can pass `null` or `raw` as the final parameter to each function::
+If you don't want the data to be escaped, you can pass ``null`` or ``'raw'`` as the final parameter to each function:
- $view->setVar('one', $one, 'raw');
+.. literalinclude:: view_renderer/004.php
If you choose not to escape data, or you are passing in an object instance, you can manually escape the data within
the view with the ``esc()`` function. The first parameter is the string to escape. The second parameter is the
@@ -78,7 +76,7 @@ Escaping Contexts
By default, the ``esc()`` and, in turn, the ``setVar()`` and ``setData()`` functions assume that the data you want to
escape is intended to be used within standard HTML. However, if the data is intended for use in Javascript, CSS,
or in an href attribute, you would need different escaping rules to be effective. You can pass in the name of the
-context as the second parameter. Valid contexts are 'html', 'js', 'css', 'url', and 'attr'::
+context as the second parameter. Valid contexts are ``'html'``, ``'js'``, ``'css'``, ``'url'``, and ``'attr'``::
Some Link
@@ -98,8 +96,7 @@ View Renderer Options
Several options can be passed to the ``render()`` or ``renderString()`` methods:
- ``cache`` - the time in seconds, to save a view's results; ignored for renderString()
-- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath;
- ignored for renderString()
+- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath; ignored for ``renderString()``
- ``saveData`` - true if the view data parameters should be retained for subsequent calls
.. note:: ``saveData`` as defined by the interface must be a boolean, but implementing
@@ -110,7 +107,7 @@ Class Reference
.. php:class:: CodeIgniter\\View\\View
- .. php:method:: render($view[, $options[, $saveData=false]])
+ .. php:method:: render($view[, $options[, $saveData = false]])
:noindex:
:param string $view: File name of the view source
@@ -119,11 +116,11 @@ Class Reference
:returns: The rendered text for the chosen view
:rtype: string
- Builds the output based upon a file name and any data that has already been set::
+ Builds the output based upon a file name and any data that has already been set:
- echo $view->render('myview');
+ .. literalinclude:: view_renderer/005.php
- .. php:method:: renderString($view[, $options[, $saveData=false]])
+ .. php:method:: renderString($view[, $options[, $saveData = false]])
:noindex:
:param string $view: Contents of the view to render, for instance content retrieved from a database
@@ -132,16 +129,16 @@ Class Reference
:returns: The rendered text for the chosen view
:rtype: string
- Builds the output based upon a view fragment and any data that has already been set::
+ Builds the output based upon a view fragment and any data that has already been set:
- echo $view->renderString('
My Sharona
');
+ .. literalinclude:: view_renderer/006.php
- This could be used for displaying content that might have been stored in a database,
+ .. warning:: This could be used for displaying content that might have been stored in a database,
but you need to be aware that this is a potential security vulnerability,
and that you **must** validate any such data, and probably escape it
appropriately!
- .. php:method:: setData([$data[, $context=null]])
+ .. php:method:: setData([$data[, $context = null]])
:noindex:
:param array $data: Array of view data strings, as key/value pairs
@@ -149,17 +146,17 @@ Class Reference
:returns: The Renderer, for method chaining
:rtype: CodeIgniter\\View\\RendererInterface.
- Sets several pieces of view data at once::
+ Sets several pieces of view data at once:
- $view->setData(['name'=>'George', 'position'=>'Boss']);
+ .. literalinclude:: view_renderer/007.php
- Supported escape contexts: html, css, js, url, or attr or raw.
- If 'raw', no escaping will happen.
+ Supported escape contexts: ``html``, ``css``, ``js``, ``url``, or ``attr`` or ``raw``.
+ If ``'raw'``, no escaping will happen.
Each call adds to the array of data that the object is accumulating,
until the view is rendered.
- .. php:method:: setVar($name[, $value=null[, $context=null]])
+ .. php:method:: setVar($name[, $value = null[, $context = null]])
:noindex:
:param string $name: Name of the view data variable
@@ -168,12 +165,12 @@ Class Reference
:returns: The Renderer, for method chaining
:rtype: CodeIgniter\\View\\RendererInterface.
- Sets a single piece of view data::
+ Sets a single piece of view data:
- $view->setVar('name','Joe','html');
+ .. literalinclude:: view_renderer/008.php
- Supported escape contexts: html, css, js, url, attr or raw.
- If 'raw', no escaping will happen.
+ Supported escape contexts: ``html``, ``css``, ``js``, ``url``, ``attr`` or ``raw``.
+ If ``'raw'``, no escaping will happen.
If you use the a view data variable that you have previously used
for this object, the new value will replace the existing one.
diff --git a/user_guide_src/source/outgoing/view_renderer/001.php b/user_guide_src/source/outgoing/view_renderer/001.php
new file mode 100644
index 000000000000..2772e22cad0d
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/001.php
@@ -0,0 +1,3 @@
+setVar('one', $one)
+ ->setVar('two', $two)
+ ->render('myView');
diff --git a/user_guide_src/source/outgoing/view_renderer/004.php b/user_guide_src/source/outgoing/view_renderer/004.php
new file mode 100644
index 000000000000..b690f3478c82
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/004.php
@@ -0,0 +1,3 @@
+setVar('one', $one, 'raw');
diff --git a/user_guide_src/source/outgoing/view_renderer/005.php b/user_guide_src/source/outgoing/view_renderer/005.php
new file mode 100644
index 000000000000..9bb35502f352
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/005.php
@@ -0,0 +1,3 @@
+render('myview');
diff --git a/user_guide_src/source/outgoing/view_renderer/006.php b/user_guide_src/source/outgoing/view_renderer/006.php
new file mode 100644
index 000000000000..8f3c21abd99d
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/006.php
@@ -0,0 +1,3 @@
+renderString('
My Sharona
');
diff --git a/user_guide_src/source/outgoing/view_renderer/007.php b/user_guide_src/source/outgoing/view_renderer/007.php
new file mode 100644
index 000000000000..99080056b1bc
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/007.php
@@ -0,0 +1,3 @@
+setData(['name' => 'George', 'position' => 'Boss']);
diff --git a/user_guide_src/source/outgoing/view_renderer/008.php b/user_guide_src/source/outgoing/view_renderer/008.php
new file mode 100644
index 000000000000..f0b868b54bca
--- /dev/null
+++ b/user_guide_src/source/outgoing/view_renderer/008.php
@@ -0,0 +1,3 @@
+setVar('name', 'Joe', 'html');
diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst
index 96e350216ce7..3eb80427fc2a 100644
--- a/user_guide_src/source/outgoing/views.rst
+++ b/user_guide_src/source/outgoing/views.rst
@@ -14,7 +14,7 @@ Views are never called directly, they must be loaded by a controller. Remember t
the Controller acts as the traffic cop, so it is responsible for fetching a particular view. If you have
not read the :doc:`Controllers ` page, you should do so before continuing.
-Using the example controller you created in the controller page, let’s add a view to it.
+Using the example controller you created in the controller page, let's add a view to it.
Creating a View
===============
@@ -35,60 +35,30 @@ Then save the file in your **app/Views** directory.
Displaying a View
=================
-To load and display a particular view file you will use the following function::
+To load and display a particular view file you will use the following function:
- echo view('name');
+.. literalinclude:: views/001.php
Where *name* is the name of your view file.
.. important:: If the file extension is omitted, then the views are expected to end with the .php extension.
-Now, open the controller file you made earlier called ``Blog.php``, and replace the echo statement with the view function::
+Now, open the controller file you made earlier called ``Blog.php``, and replace the echo statement with the view function:
- 'Your title',
- ];
+content view, and a footer view. That might look something like this:
- echo view('header');
- echo view('menu');
- echo view('content', $data);
- echo view('footer');
- }
- }
+.. literalinclude:: views/003.php
In the example above, we are using "dynamically added data", which you will see below.
@@ -96,9 +66,9 @@ Storing Views within Sub-directories
====================================
Your view files can also be stored within sub-directories if you prefer that type of organization.
-When doing so you will need to include the directory name loading the view. Example::
+When doing so you will need to include the directory name loading the view. Example:
- echo view('directory_name/file_name');
+.. literalinclude:: views/004.php
Namespaced Views
================
@@ -109,9 +79,9 @@ to package your views together in a module-like fashion for easy re-use or distr
If you have ``example/blog`` directory that has a PSR-4 mapping set up in the :doc:`Autoloader ` living
under the namespace ``Example\Blog``, you could retrieve view files as if they were namespaced also. Following this
-example, you could load the **blog_view.php** file from **example/blog/Views** by prepending the namespace to the view name::
+example, you could load the **blog_view.php** file from **example/blog/Views** by prepending the namespace to the view name:
- echo view('Example\Blog\Views\blog_view');
+.. literalinclude:: views/005.php
.. _caching-views:
@@ -119,47 +89,26 @@ Caching Views
=============
You can cache a view with the ``view`` command by passing a ``cache`` option with the number of seconds to cache
-the view for, in the third parameter::
+the view for, in the third parameter:
- // Cache the view for 60 seconds
- echo view('file_name', $data, ['cache' => 60]);
+.. literalinclude:: views/006.php
By default, the view will be cached using the same name as the view file itself. You can customize this by passing
-along ``cache_name`` and the cache ID you wish to use::
+along ``cache_name`` and the cache ID you wish to use:
- // Cache the view for 60 seconds
- echo view('file_name', $data, ['cache' => 60, 'cache_name' => 'my_cached_view']);
+.. literalinclude:: views/007.php
Adding Dynamic Data to the View
===============================
-Data is passed from the controller to the view by way of an array in the second parameter of the view function.
-Here's an example::
+Data is passed from the controller to the view by way of an array in the second parameter of the ``view()`` function.
+Here's an example:
- $data = [
- 'title' => 'My title',
- 'heading' => 'My Heading',
- 'message' => 'My Message',
- ];
+.. literalinclude:: views/008.php
- echo view('blog_view', $data);
+Let's try it with your controller file. Open it and add this code:
-Let's try it with your controller file. Open it and add this code::
-
- 'My title',
- 'heading' => 'My Heading',
- 'message' => 'My Message',
- ];
+But this might not keep any data from "bleeding" into
+other views, potentially causing issues. If you would prefer to clean the data after one call, you can pass the ``saveData`` option
+into the ``$option`` array in the third parameter.
- echo view('blog_view', $data, ['saveData' => true]);
+.. literalinclude:: views/010.php
-Additionally, if you would like the default functionality of the view function to be that it does save the data
-between calls, you can set ``$saveData`` to **true** in **app/Config/Views.php**.
+Additionally, if you would like the default functionality of the ``view()`` function to be that it does clear the data
+between calls, you can set ``$saveData`` to **false** in **app/Config/Views.php**.
Creating Loops
==============
@@ -198,44 +145,10 @@ The data array you pass to your view files is not limited to simple variables. Y
arrays, which can be looped to generate multiple rows. For example, if you pull data from your database it will
typically be in the form of a multi-dimensional array.
-Here’s a simple example. Add this to your controller::
+Here's a simple example. Add this to your controller:
- ['Clean House', 'Call Mom', 'Run Errands'],
- 'title' => 'My Real Title',
- 'heading' => 'My Real Heading',
- ];
-
- echo view('blog_view', $data);
- }
- }
-
-Now open your view file and create a loop::
-
-
-
- = esc($title) ?>
-
-
-
+
+
+
diff --git a/user_guide_src/source/testing/benchmark.rst b/user_guide_src/source/testing/benchmark.rst
index 780dcfe8ac08..1e7a5c9607b7 100644
--- a/user_guide_src/source/testing/benchmark.rst
+++ b/user_guide_src/source/testing/benchmark.rst
@@ -23,48 +23,34 @@ it simple to measure the performance of different aspects of your application. A
the ``start()`` and ``stop()`` methods.
The ``start()`` methods takes a single parameter: the name of this timer. You can use any string as the name
-of the timer. It is only used for you to reference later to know which measurement is which::
+of the timer. It is only used for you to reference later to know which measurement is which:
- $benchmark = \Config\Services::timer();
- $benchmark->start('render view');
+.. literalinclude:: benchmark/001.php
-The ``stop()`` method takes the name of the timer that you want to stop as the only parameter, also::
+The ``stop()`` method takes the name of the timer that you want to stop as the only parameter, also:
- $benchmark->stop('render view');
+.. literalinclude:: benchmark/002.php
The name is not case-sensitive, but otherwise must match the name you gave it when you started the timer.
Alternatively, you can use the :doc:`global function ` ``timer()`` to start
-and stop timers::
+and stop timers:
- // Start the timer
- timer('render view');
- // Stop a running timer,
- // if one of this name has been started
- timer('render view');
+.. literalinclude:: benchmark/003.php
Viewing Your Benchmark Points
=============================
When your application runs, all of the timers that you have set are collected by the Timer class. It does
not automatically display them, though. You can retrieve all of your timers by calling the ``getTimers()`` method.
-This returns an array of benchmark information, including start, end, and duration::
+This returns an array of benchmark information, including start, end, and duration:
- $timers = $benchmark->getTimers();
-
- // Timers =
- [
- 'render view' => [
- 'start' => 1234567890,
- 'end' => 1345678920,
- 'duration' => 15.4315, // number of seconds
- ]
- ]
+.. literalinclude:: benchmark/004.php
You can change the precision of the calculated duration by passing in the number of decimal places you want to be shown as
-the only parameter. The default value is 4 numbers behind the decimal point::
+the only parameter. The default value is 4 numbers behind the decimal point:
- $timers = $benchmark->getTimers(6);
+.. literalinclude:: benchmark/005.php
The timers are automatically displayed in the :doc:`Debub Toolbar `.
@@ -73,10 +59,9 @@ Displaying Execution Time
While the ``getTimers()`` method will give you the raw data for all of the timers in your project, you can retrieve
the duration of a single timer, in seconds, with the `getElapsedTime()` method. The first parameter is the name of
-the timer to display. The second is the number of decimal places to display. This defaults to 4::
+the timer to display. The second is the number of decimal places to display. This defaults to 4:
- echo timer()->getElapsedTime('render view');
- // Displays: 0.0234
+.. literalinclude:: benchmark/006.php
==================
Using the Iterator
@@ -92,32 +77,20 @@ Creating Tasks To Run
Tasks are defined within Closures. Any output the task creates will be discarded automatically. They are
added to the Iterator class through the `add()` method. The first parameter is a name you want to refer to
-this test by. The second parameter is the Closure, itself::
-
- $iterator = new \CodeIgniter\Benchmark\Iterator();
-
- // Add a new task
- $iterator->add('single_concat', function () {
- $str = 'Some basic'.'little'.'string concatenation test.';
- });
+this test by. The second parameter is the Closure, itself:
- // Add another task
- $iterator->add('double', function ($a = 'little') {
- $str = "Some basic {$little} string test.";
- });
+.. literalinclude:: benchmark/007.php
Running the Tasks
=================
Once you've added the tasks to run, you can use the ``run()`` method to loop over the tasks many times.
By default, it will run each task 1000 times. This is probably sufficient for most simple tests. If you need
-to run the tests more times than that, you can pass the number as the first parameter::
+to run the tests more times than that, you can pass the number as the first parameter:
- // Run the tests 3000 times.
- $iterator->run(3000);
+.. literalinclude:: benchmark/008.php
Once it has run, it will return an HTML table with the results of the test. If you don't want the results
-displayed, you can pass in `false` as the second parameter::
+displayed, you can pass in `false` as the second parameter:
- // Don't display the results.
- $iterator->run(1000, false);
+.. literalinclude:: benchmark/009.php
diff --git a/user_guide_src/source/testing/benchmark/001.php b/user_guide_src/source/testing/benchmark/001.php
new file mode 100644
index 000000000000..ce3bd09ac87f
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/001.php
@@ -0,0 +1,4 @@
+start('render view');
diff --git a/user_guide_src/source/testing/benchmark/002.php b/user_guide_src/source/testing/benchmark/002.php
new file mode 100644
index 000000000000..003d1cc18cf7
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/002.php
@@ -0,0 +1,3 @@
+stop('render view');
diff --git a/user_guide_src/source/testing/benchmark/003.php b/user_guide_src/source/testing/benchmark/003.php
new file mode 100644
index 000000000000..861dbd8232a5
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/003.php
@@ -0,0 +1,7 @@
+getTimers();
+/*
+ * Produces:
+ * [
+ * 'render view' => [
+ * 'start' => 1234567890,
+ * 'end' => 1345678920,
+ * 'duration' => 15.4315, // number of seconds
+ * ]
+ * ]
+ */
diff --git a/user_guide_src/source/testing/benchmark/005.php b/user_guide_src/source/testing/benchmark/005.php
new file mode 100644
index 000000000000..309c02f92a9b
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/005.php
@@ -0,0 +1,3 @@
+getTimers(6);
diff --git a/user_guide_src/source/testing/benchmark/006.php b/user_guide_src/source/testing/benchmark/006.php
new file mode 100644
index 000000000000..a5f93a20fbf9
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/006.php
@@ -0,0 +1,4 @@
+getElapsedTime('render view');
+// Displays: 0.0234
diff --git a/user_guide_src/source/testing/benchmark/007.php b/user_guide_src/source/testing/benchmark/007.php
new file mode 100644
index 000000000000..13a95ec66e29
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/007.php
@@ -0,0 +1,13 @@
+add('single_concat', static function () {
+ $str = 'Some basic' . 'little' . 'string concatenation test.';
+});
+
+// Add another task
+$iterator->add('double', static function ($a = 'little') {
+ $str = "Some basic {$little} string test.";
+});
diff --git a/user_guide_src/source/testing/benchmark/008.php b/user_guide_src/source/testing/benchmark/008.php
new file mode 100644
index 000000000000..da0f6a83b5fa
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/008.php
@@ -0,0 +1,4 @@
+run(3000);
diff --git a/user_guide_src/source/testing/benchmark/009.php b/user_guide_src/source/testing/benchmark/009.php
new file mode 100644
index 000000000000..1f30692559b2
--- /dev/null
+++ b/user_guide_src/source/testing/benchmark/009.php
@@ -0,0 +1,4 @@
+run(1000, false);
diff --git a/user_guide_src/source/testing/controllers.rst b/user_guide_src/source/testing/controllers.rst
index d5dd2d379b8b..113c5ef2b66e 100644
--- a/user_guide_src/source/testing/controllers.rst
+++ b/user_guide_src/source/testing/controllers.rst
@@ -17,146 +17,97 @@ case you need it.
The Helper Trait
================
-To enable Controller Testing you need to use the ``ControllerTestTrait`` trait within your tests::
+To enable Controller Testing you need to use the ``ControllerTestTrait`` trait within your tests:
- withURI('http://example.com/categories')
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
-
- $this->assertTrue($result->isOK());
- }
- }
+.. literalinclude:: controllers/002.php
Helper Methods
==============
-**controller($class)**
+controller($class)
+------------------
Specifies the class name of the controller to test. The first parameter must be a fully qualified class name
-(i.e., include the namespace)::
+(i.e., include the namespace):
- $this->controller(\App\Controllers\ForumController::class);
+.. literalinclude:: controllers/003.php
-**execute(string $method, ...$params)**
+execute(string $method, ...$params)
+-----------------------------------
-Executes the specified method within the controller. The first parameter is the name of the method to run::
+Executes the specified method within the controller. The first parameter is the name of the method to run:
- $results = $this->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/004.php
By specifying the second and subsequent parameters, you can pass them to the controller method.
This returns a new helper class that provides a number of routines for checking the response itself. See below
for details.
-**withConfig($config)**
-
-Allows you to pass in a modified version of **Config\App.php** to test with different settings::
+withConfig($config)
+-------------------
- $config = new Config\App();
- $config->appTimezone = 'America/Chicago';
+Allows you to pass in a modified version of **Config\App.php** to test with different settings:
- $results = $this->withConfig($config)
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/005.php
If you do not provide one, the application's App config file will be used.
-**withRequest($request)**
+withRequest($request)
+---------------------
-Allows you to provide an **IncomingRequest** instance tailored to your testing needs::
+Allows you to provide an **IncomingRequest** instance tailored to your testing needs:
- $request = new \CodeIgniter\HTTP\IncomingRequest(new \Config\App(), new URI('http://example.com'));
- $request->setLocale($locale);
-
- $results = $this->withRequest($request)
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/006.php
If you do not provide one, a new IncomingRequest instance with the default application values will be passed
into your controller.
-**withResponse($response)**
-
-Allows you to provide a **Response** instance::
+withResponse($response)
+-----------------------
- $response = new \CodeIgniter\HTTP\Response(new \Config\App());
+Allows you to provide a **Response** instance:
- $results = $this->withResponse($response)
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/007.php
If you do not provide one, a new Response instance with the default application values will be passed
into your controller.
-**withLogger($logger)**
-
-Allows you to provide a **Logger** instance::
+withLogger($logger)
+-------------------
- $logger = new \CodeIgniter\Log\Handlers\FileHandler();
+Allows you to provide a **Logger** instance:
- $results = $this->withResponse($response)
- ->withLogger($logger)
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/008.php
If you do not provide one, a new Logger instance with the default configuration values will be passed
into your controller.
-**withURI(string $uri)**
+withURI(string $uri)
+--------------------
Allows you to provide a new URI that simulates the URL the client was visiting when this controller was run.
This is helpful if you need to check URI segments within your controller. The only parameter is a string
-representing a valid URI::
+representing a valid URI:
- $results = $this->withURI('http://example.com/forums/categories')
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/009.php
It is a good practice to always provide the URI during testing to avoid surprises.
-**withBody($body)**
+withBody($body)
+---------------
Allows you to provide a custom body for the request. This can be helpful when testing API controllers where
-you need to set a JSON value as the body. The only parameter is a string that represents the body of the request::
-
- $body = json_encode(['foo' => 'bar']);
+you need to set a JSON value as the body. The only parameter is a string that represents the body of the request:
- $results = $this->withBody($body)
- ->controller(\App\Controllers\ForumController::class)
- ->execute('showCategories');
+.. literalinclude:: controllers/010.php
Checking the Response
=====================
@@ -174,19 +125,9 @@ The Helper Trait
----------------
Just like with the Controller Tester you need to include the ``FilterTestTrait`` in your test
-cases to enable these features::
-
- filtersConfig->globals['before'] = ['admin-only-filter'];
-
- $this->assertHasFilters('unfiltered/route', 'before');
- }
- ...
+.. literalinclude:: controllers/012.php
Checking Routes
---------------
@@ -234,9 +165,9 @@ a large performance advantage over Controller and HTTP Testing.
:returns: Aliases for each filter that would have run
:rtype: string[]
- Usage example::
+ Usage example:
- $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar']
+ .. literalinclude:: controllers/013.php
Calling Filter Methods
----------------------
@@ -252,15 +183,9 @@ method using these properties to test your Filter code safely and check the resu
:returns: A callable method to run the simulated Filter event
:rtype: Closure
- Usage example::
-
- protected function testUnauthorizedAccessRedirects()
- {
- $caller = $this->getFilterCaller('permission', 'before');
- $result = $caller('MayEditWidgets');
+ Usage example:
- $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result);
- }
+ .. literalinclude:: controllers/014.php
Notice how the ``Closure`` can take input parameters which are passed to your filter method.
@@ -270,22 +195,30 @@ Assertions
In addition to the helper methods above ``FilterTestTrait`` also comes with some assertions
to streamline your test methods.
-The **assertFilter()** method checks that the given route at position uses the filter (by its alias)::
+assertFilter()
+^^^^^^^^^^^^^^
+
+The ``assertFilter()`` method checks that the given route at position uses the filter (by its alias):
+
+.. literalinclude:: controllers/015.php
+
+assertNotFilter()
+^^^^^^^^^^^^^^^^^
+
+The ``assertNotFilter()`` method checks that the given route at position does not use the filter (by its alias):
- // Make sure users are logged in before checking their account
- $this->assertFilter('users/account', 'before', 'login');
+.. literalinclude:: controllers/016.php
-The **assertNotFilter()** method checks that the given route at position does not use the filter (by its alias)::
+assertHasFilters()
+^^^^^^^^^^^^^^^^^^
- // Make sure API calls do not try to use the Debug Toolbar
- $this->assertNotFilter('api/v1/widgets', 'after', 'toolbar');
+The ``assertHasFilters()`` method checks that the given route at position has at least one filter set:
-The **assertHasFilters()** method checks that the given route at position has at least one filter set::
+.. literalinclude:: controllers/017.php
- // Make sure that filters are enabled
- $this->assertHasFilters('filtered/route', 'after');
+assertNotHasFilters()
+^^^^^^^^^^^^^^^^^^^^^
-The **assertNotHasFilters()** method checks that the given route at position has no filters set::
+The ``assertNotHasFilters()`` method checks that the given route at position has no filters set:
- // Make sure no filters run for our static pages
- $this->assertNotHasFilters('about/contact', 'before');
+.. literalinclude:: controllers/018.php
diff --git a/user_guide_src/source/testing/controllers/001.php b/user_guide_src/source/testing/controllers/001.php
new file mode 100644
index 000000000000..9162fa0aeabd
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/001.php
@@ -0,0 +1,13 @@
+withURI('http://example.com/categories')
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
+
+ $this->assertTrue($result->isOK());
+ }
+}
diff --git a/user_guide_src/source/testing/controllers/003.php b/user_guide_src/source/testing/controllers/003.php
new file mode 100644
index 000000000000..51e810538e87
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/003.php
@@ -0,0 +1,3 @@
+controller(\App\Controllers\ForumController::class);
diff --git a/user_guide_src/source/testing/controllers/004.php b/user_guide_src/source/testing/controllers/004.php
new file mode 100644
index 000000000000..17b5f42590ce
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/004.php
@@ -0,0 +1,4 @@
+controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/005.php b/user_guide_src/source/testing/controllers/005.php
new file mode 100644
index 000000000000..7d2864f5fcbf
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/005.php
@@ -0,0 +1,8 @@
+appTimezone = 'America/Chicago';
+
+$results = $this->withConfig($config)
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/006.php b/user_guide_src/source/testing/controllers/006.php
new file mode 100644
index 000000000000..0fc7670487de
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/006.php
@@ -0,0 +1,8 @@
+setLocale($locale);
+
+$results = $this->withRequest($request)
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/007.php b/user_guide_src/source/testing/controllers/007.php
new file mode 100644
index 000000000000..8f7d55448035
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/007.php
@@ -0,0 +1,7 @@
+withResponse($response)
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/008.php b/user_guide_src/source/testing/controllers/008.php
new file mode 100644
index 000000000000..641a7f92f68b
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/008.php
@@ -0,0 +1,8 @@
+withResponse($response)
+ ->withLogger($logger)
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/009.php b/user_guide_src/source/testing/controllers/009.php
new file mode 100644
index 000000000000..711d947b4267
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/009.php
@@ -0,0 +1,5 @@
+withURI('http://example.com/forums/categories')
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/010.php b/user_guide_src/source/testing/controllers/010.php
new file mode 100644
index 000000000000..e257dcbc4928
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/010.php
@@ -0,0 +1,7 @@
+ 'bar']);
+
+$results = $this->withBody($body)
+ ->controller(\App\Controllers\ForumController::class)
+ ->execute('showCategories');
diff --git a/user_guide_src/source/testing/controllers/011.php b/user_guide_src/source/testing/controllers/011.php
new file mode 100644
index 000000000000..f2ab558b8da2
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/011.php
@@ -0,0 +1,11 @@
+filtersConfig->globals['before'] = ['admin-only-filter'];
+
+ $this->assertHasFilters('unfiltered/route', 'before');
+ }
+
+ // ...
+}
diff --git a/user_guide_src/source/testing/controllers/013.php b/user_guide_src/source/testing/controllers/013.php
new file mode 100644
index 000000000000..e205f0f4ea54
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/013.php
@@ -0,0 +1,3 @@
+getFiltersForRoute('/', 'after'); // ['toolbar']
diff --git a/user_guide_src/source/testing/controllers/014.php b/user_guide_src/source/testing/controllers/014.php
new file mode 100644
index 000000000000..feb1d4448540
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/014.php
@@ -0,0 +1,19 @@
+getFilterCaller('permission', 'before');
+ $result = $caller('MayEditWidgets');
+
+ $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result);
+ }
+}
diff --git a/user_guide_src/source/testing/controllers/015.php b/user_guide_src/source/testing/controllers/015.php
new file mode 100644
index 000000000000..866d2e80ce43
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/015.php
@@ -0,0 +1,4 @@
+assertFilter('users/account', 'before', 'login');
diff --git a/user_guide_src/source/testing/controllers/016.php b/user_guide_src/source/testing/controllers/016.php
new file mode 100644
index 000000000000..cdbf08ccc36b
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/016.php
@@ -0,0 +1,4 @@
+assertNotFilter('api/v1/widgets', 'after', 'toolbar');
diff --git a/user_guide_src/source/testing/controllers/017.php b/user_guide_src/source/testing/controllers/017.php
new file mode 100644
index 000000000000..15db694e5419
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/017.php
@@ -0,0 +1,4 @@
+assertHasFilters('filtered/route', 'after');
diff --git a/user_guide_src/source/testing/controllers/018.php b/user_guide_src/source/testing/controllers/018.php
new file mode 100644
index 000000000000..33606c0e122e
--- /dev/null
+++ b/user_guide_src/source/testing/controllers/018.php
@@ -0,0 +1,4 @@
+assertNotHasFilters('about/contact', 'before');
diff --git a/user_guide_src/source/testing/database.rst b/user_guide_src/source/testing/database.rst
index 86b91537917e..382b4ee4aa56 100644
--- a/user_guide_src/source/testing/database.rst
+++ b/user_guide_src/source/testing/database.rst
@@ -1,63 +1,27 @@
-=====================
+#####################
Testing Your Database
-=====================
+#####################
.. contents::
:local:
:depth: 2
The Test Class
-==============
+**************
In order to take advantage of the built-in database tools that CodeIgniter provides for testing, your
-tests must extend ``CIUnitTestCase`` and use the ``DatabaseTestTrait``::
+tests must extend ``CIUnitTestCase`` and use the ``DatabaseTestTrait``:
- 'joe@example.com',
- 'active' => 1,
- ];
- $this->dontSeeInDatabase('users', $criteria);
-
-**seeInDatabase($table, $criteria)**
+Inserts a new row into the database. This row is removed after the current test runs. ``$data`` is an associative
+array with the data to insert into the table.
-Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES exist in the database.
-::
+.. literalinclude:: database/007.php
- $criteria = [
- 'email' => 'joe@example.com',
- 'active' => 1,
- ];
- $this->seeInDatabase('users', $criteria);
+Getting Data from Database
+==========================
-**grabFromDatabase($table, $column, $criteria)**
+grabFromDatabase($table, $column, $criteria)
+--------------------------------------------
Returns the value of ``$column`` from the specified table where the row matches ``$criteria``. If more than one
-row is found, it will only test against the first one.
-::
+row is found, it will only return the first one.
- $username = $this->grabFromDatabase('users', 'username', ['email' => 'joe@example.com']);
+Assertions
+==========
-**hasInDatabase($table, $data)**
+dontSeeInDatabase($table, $criteria)
+------------------------------------
-Inserts a new row into the database. This row is removed after the current test runs. ``$data`` is an associative
-array with the data to insert into the table.
-::
+Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES NOT exist in the database.
+
+.. literalinclude:: database/004.php
+
+seeInDatabase($table, $criteria)
+--------------------------------
+
+Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES exist in the database.
- $data = [
- 'email' => 'joe@example.com',
- 'name' => 'Joe Cool',
- ];
- $this->hasInDatabase('users', $data);
+.. literalinclude:: database/005.php
-**seeNumRecords($expected, $table, $criteria)**
+seeNumRecords($expected, $table, $criteria)
+-------------------------------------------
Asserts that a number of matching rows are found in the database that match ``$criteria``.
-::
- $criteria = [
- 'active' => 1,
- ];
- $this->seeNumRecords(2, 'users', $criteria);
+.. literalinclude:: database/008.php
diff --git a/user_guide_src/source/testing/database/001.php b/user_guide_src/source/testing/database/001.php
new file mode 100644
index 000000000000..809cd59d8c0f
--- /dev/null
+++ b/user_guide_src/source/testing/database/001.php
@@ -0,0 +1,13 @@
+ 'joe@example.com',
+ 'active' => 1,
+];
+$this->dontSeeInDatabase('users', $criteria);
diff --git a/user_guide_src/source/testing/database/005.php b/user_guide_src/source/testing/database/005.php
new file mode 100644
index 000000000000..960ba5e5b91e
--- /dev/null
+++ b/user_guide_src/source/testing/database/005.php
@@ -0,0 +1,7 @@
+ 'joe@example.com',
+ 'active' => 1,
+];
+$this->seeInDatabase('users', $criteria);
diff --git a/user_guide_src/source/testing/database/006.php b/user_guide_src/source/testing/database/006.php
new file mode 100644
index 000000000000..951736b95611
--- /dev/null
+++ b/user_guide_src/source/testing/database/006.php
@@ -0,0 +1,3 @@
+grabFromDatabase('users', 'username', ['email' => 'joe@example.com']);
diff --git a/user_guide_src/source/testing/database/007.php b/user_guide_src/source/testing/database/007.php
new file mode 100644
index 000000000000..46ebb8fbe695
--- /dev/null
+++ b/user_guide_src/source/testing/database/007.php
@@ -0,0 +1,7 @@
+ 'joe@example.com',
+ 'name' => 'Joe Cool',
+];
+$this->hasInDatabase('users', $data);
diff --git a/user_guide_src/source/testing/database/008.php b/user_guide_src/source/testing/database/008.php
new file mode 100644
index 000000000000..9a60e154d331
--- /dev/null
+++ b/user_guide_src/source/testing/database/008.php
@@ -0,0 +1,6 @@
+ 1,
+];
+$this->seeNumRecords(2, 'users', $criteria);
diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst
index 22c77141e116..b8da022d018f 100644
--- a/user_guide_src/source/testing/debugging.rst
+++ b/user_guide_src/source/testing/debugging.rst
@@ -26,22 +26,25 @@ This is defined in the boot files (e.g. **app/Config/Boot/development.php**).
Using Kint
==========
-**d()**
+d()
+---
The ``d()`` method dumps all of the data it knows about the contents passed as the only parameter to the screen, and
-allows the script to continue executing::
+allows the script to continue executing:
- d($_SERVER);
+.. literalinclude:: debugging/001.php
-**dd()**
+dd()
+----
-This method is identical to ``d()``, except that it also ``dies()`` and no further code is executed this request.
+This method is identical to ``d()``, except that it also ``die()`` and no further code is executed this request.
-**trace()**
+trace()
+-------
-This provides a backtrace to the current execution point, with Kint's own unique spin::
+This provides a backtrace to the current execution point, with Kint's own unique spin:
- trace();
+.. literalinclude:: debugging/002.php
For more information, see `Kint's page `_.
@@ -74,18 +77,9 @@ Choosing What to Show
CodeIgniter ships with several Collectors that, as the name implies, collect data to display on the toolbar. You
can easily make your own to customize the toolbar. To determine which collectors are shown, again head over to
-the **app/Config/Toolbar.php** configuration file::
-
- public $collectors = [
- \CodeIgniter\Debug\Toolbar\Collectors\Timers::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Database::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Logs::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Views::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Files::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Routes::class,
- \CodeIgniter\Debug\Toolbar\Collectors\Events::class,
- ];
+the **app/Config/Toolbar.php** configuration file:
+
+.. literalinclude:: debugging/003.php
Comment out any collectors that you do not want to show. Add custom Collectors here by providing the fully-qualified
class name. The exact collectors that appear here will affect which tabs are shown, as well as what information is
@@ -119,24 +113,8 @@ Creating custom collectors is a straightforward task. You create a new class, fu
can locate it, that extends ``CodeIgniter\Debug\Toolbar\Collectors\BaseCollector``. This provides a number of methods
that you can override, and has four required class properties that you must correctly set depending on how you want
the Collector to work
-::
-
- '', // Name displayed on the left of the timeline
- 'component' => '', // Name of the Component listed in the middle of timeline
- 'start' => 0.00, // start time, like microtime(true)
- 'duration' => 0.00, // duration, like mircrotime(true) - microtime(true)
- ];
+.. literalinclude:: debugging/005.php
Providing Vars
--------------
@@ -196,15 +169,6 @@ To add data to the Vars tab you must:
2. Implement ``getVarData()`` method.
The ``getVarData()`` method should return an array containing arrays of key/value pairs to display. The name of the
-outer array's key is the name of the section on the Vars tab::
-
- $data = [
- 'section 1' => [
- 'foo' => 'bar',
- 'bar' => 'baz',
- ],
- 'section 2' => [
- 'foo' => 'bar',
- 'bar' => 'baz',
- ],
- ];
+outer array's key is the name of the section on the Vars tab:
+
+.. literalinclude:: debugging/006.php
diff --git a/user_guide_src/source/testing/debugging/001.php b/user_guide_src/source/testing/debugging/001.php
new file mode 100644
index 000000000000..f54b4701f98e
--- /dev/null
+++ b/user_guide_src/source/testing/debugging/001.php
@@ -0,0 +1,3 @@
+ '', // Name displayed on the left of the timeline
+ 'component' => '', // Name of the Component listed in the middle of timeline
+ 'start' => 0.00, // start time, like microtime(true)
+ 'duration' => 0.00, // duration, like mircrotime(true) - microtime(true)
+];
diff --git a/user_guide_src/source/testing/debugging/006.php b/user_guide_src/source/testing/debugging/006.php
new file mode 100644
index 000000000000..3ad25be67894
--- /dev/null
+++ b/user_guide_src/source/testing/debugging/006.php
@@ -0,0 +1,12 @@
+ [
+ 'foo' => 'bar',
+ 'bar' => 'baz',
+ ],
+ 'section 2' => [
+ 'foo' => 'bar',
+ 'bar' => 'baz',
+ ],
+];
diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst
index 933db982b2ee..826e435d0821 100644
--- a/user_guide_src/source/testing/fabricator.rst
+++ b/user_guide_src/source/testing/fabricator.rst
@@ -3,7 +3,7 @@ Generating Test Data
####################
Often you will need sample data for your application to run its tests. The ``Fabricator`` class
-uses fzaninotto's `Faker `_ to turn models into generators
+uses fzaninotto's `Faker `_ to turn models into generators
of random data. Use fabricators in your seeds or test cases to stage fake data for your unit tests.
.. contents::
@@ -14,27 +14,22 @@ Supported Models
================
``Fabricator`` supports any model that extends the framework's core model, ``CodeIgniter\Model``.
-You may use your own custom models by ensuring they implement ``CodeIgniter\Test\Interfaces\FabricatorModel``::
+You may use your own custom models by ensuring they implement ``CodeIgniter\Test\Interfaces\FabricatorModel``:
- class MyModel implements CodeIgniter\Test\Interfaces\FabricatorModel
+.. literalinclude:: fabricator/001.php
.. note:: In addition to methods, the interface outlines some necessary properties for the target model. Please see the interface code for details.
Loading Fabricators
===================
-At its most basic a fabricator takes the model to act on::
+At its most basic a fabricator takes the model to act on:
- use App\Models\UserModel;
- use CodeIgniter\Test\Fabricator;
+.. literalinclude:: fabricator/002.php
- $fabricator = new Fabricator(UserModel::class);
+The parameter can be a string specifying the name of the model, or an instance of the model itself:
-The parameter can be a string specifying the name of the model, or an instance of the model itself::
-
- $model = new UserModel($testDbConnection);
-
- $fabricator = new Fabricator($model);
+.. literalinclude:: fabricator/003.php
Defining Formatters
===================
@@ -43,58 +38,36 @@ Faker generates data by requesting it from a formatter. With no formatters defin
attempt to guess at the most appropriate fit based on the field name and properties of the model it
represents, falling back on ``$fabricator->defaultFormatter``. This may be fine if your field names
correspond with common formatters, or if you don't care much about the content of the fields, but most
-of the time you will want to specify the formatters to use as the second parameter to the constructor::
-
- $formatters = [
- 'first' => 'firstName',
- 'email' => 'email',
- 'phone' => 'phoneNumber',
- 'avatar' => 'imageUrl',
- ];
+of the time you will want to specify the formatters to use as the second parameter to the constructor:
- $fabricator = new Fabricator(UserModel::class, $formatters);
+.. literalinclude:: fabricator/004.php
You can also change the formatters after a fabricator is initialized by using the ``setFormatters()`` method.
-**Advanced Formatting**
+Advanced Formatting
+-------------------
Sometimes the default return of a formatter is not enough. Faker providers allow parameters to most formatters
to further limit the scope of random data. A fabricator will check its representative model for the ``fake()``
-method where you can define exactly what the faked data should look like::
-
- class UserModel
- {
- public function fake(Generator &$faker)
- {
- return [
- 'first' => $faker->firstName,
- 'email' => $faker->email,
- 'phone' => $faker->phoneNumber,
- 'avatar' => Faker\Provider\Image::imageUrl(800, 400),
- 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null,
- ];
- }
+method where you can define exactly what the faked data should look like:
+
+.. literalinclude:: fabricator/005.php
Notice in this example how the first three values are equivalent to the formatters from before. However for ``avatar``
we have requested an image size other than the default and ``login`` uses a conditional based on app configuration,
neither of which are possible using the ``$formatters`` parameter.
You may want to keep your test data separate from your production models, so it is a good practice to define
-a child class in your test support folder::
-
- namespace Tests\Support\Models;
+a child class in your test support folder:
- class UserFabricator extends \App\Models\UserModel
- {
- public function fake(&$faker)
- {
+.. literalinclude:: fabricator/006.php
Localization
============
Faker supports a lot of different locales. Check their documentation to determine which providers
-support your locale. Specify a locale in the third parameter while initiating a fabricator::
+support your locale. Specify a locale in the third parameter while initiating a fabricator:
- $fabricator = new Fabricator(UserModel::class, null, 'fr_FR');
+.. literalinclude:: fabricator/007.php
If no locale is specified it will use the one defined in **app/Config/App.php** as ``defaultLocale``.
You can check the locale of an existing fabricator using its ``getLocale()`` method.
@@ -102,114 +75,61 @@ You can check the locale of an existing fabricator using its ``getLocale()`` met
Faking the Data
===============
-Once you have a properly-initialized fabricator it is easy to generate test data with the ``make()`` command::
+Once you have a properly-initialized fabricator it is easy to generate test data with the ``make()`` command:
- $fabricator = new Fabricator(UserFabricator::class);
- $testUser = $fabricator->make();
- print_r($testUser);
+.. literalinclude:: fabricator/008.php
-You might get back something like this::
+You might get back something like this:
- array(
- 'first' => "Maynard",
- 'email' => "king.alford@example.org",
- 'phone' => "201-886-0269 x3767",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- )
+.. literalinclude:: fabricator/009.php
-You can also get a lot of them back by supplying a count::
+You can also get a lot of them back by supplying a count:
- $users = $fabricator->make(10);
+.. literalinclude:: fabricator/010.php
The return type of ``make()`` mimics what is defined in the representative model, but you can
-force a type using the methods directly::
+force a type using the methods directly:
- $userArray = $fabricator->makeArray();
- $userObject = $fabricator->makeObject();
- $userEntity = $fabricator->makeObject('App\Entities\User');
+.. literalinclude:: fabricator/011.php
The return from ``make()`` is ready to be used in tests or inserted into the database. Alternatively
``Fabricator`` includes the ``create()`` command to insert it for you, and return the result. Due
to model callbacks, database formatting, and special keys like primary and timestamps the return
-from ``create()`` can differ from ``make()``. You might get back something like this::
+from ``create()`` can differ from ``make()``. You might get back something like this:
- array(
- 'id' => 1,
- 'first' => "Rachel",
- 'email' => "bradley72@gmail.com",
- 'phone' => "741-241-2356",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- 'created_at' => "2020-05-08 14:52:10",
- 'updated_at' => "2020-05-08 14:52:10",
- )
+.. literalinclude:: fabricator/012.php
-Similar to ``make()`` you can supply a count to insert and return an array of objects::
+Similar to ``make()`` you can supply a count to insert and return an array of objects:
- $users = $fabricator->create(100);
+.. literalinclude:: fabricator/013.php
Finally, there may be times you want to test with the full database object but you are not actually
using a database. ``create()`` takes a second parameter to allowing mocking the object, returning
-the object with extra database fields above without actually touching the database::
-
- $user = $fabricator(null, true);
+the object with extra database fields above without actually touching the database:
- $this->assertIsNumeric($user->id);
- $this->dontSeeInDatabase('user', ['id' => $user->id]);
+.. literalinclude:: fabricator/014.php
Specifying Test Data
====================
Generated data is great, but sometimes you may want to supply a specific field for a test without
compromising your formatters configuration. Rather then creating a new fabricator for each variant
-you can use ``setOverrides()`` to specify the value for any fields::
+you can use ``setOverrides()`` to specify the value for any fields:
- $fabricator->setOverrides(['first' => 'Bobby']);
- $bobbyUser = $fabricator->make();
+.. literalinclude:: fabricator/015.php
-Now any data generated with ``make()`` or ``create()`` will always use "Bobby" for the ``first`` field::
+Now any data generated with ``make()`` or ``create()`` will always use "Bobby" for the ``first`` field:
- array(
- 'first' => "Bobby",
- 'email' => "latta.kindel@company.org",
- 'phone' => "251-806-2169",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- )
-
- array(
- 'first' => "Bobby",
- 'email' => "melissa.strike@fabricon.us",
- 'phone' => "525-214-2656 x23546",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- )
+.. literalinclude:: fabricator/016.php
``setOverrides()`` can take a second parameter to indicate whether this should be a persistent
-override or only for a single action::
-
- $fabricator->setOverrides(['first' => 'Bobby'], $persist = false);
- $bobbyUser = $fabricator->make();
- $bobbyUser = $fabricator->make();
-
-Notice after the first return the fabricator stops using the overrides::
-
- array(
- 'first' => "Bobby",
- 'email' => "belingadon142@example.org",
- 'phone' => "741-857-1933 x1351",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- )
-
- array(
- 'first' => "Hans",
- 'email' => "hoppifur@metraxalon.com",
- 'phone' => "487-235-7006",
- 'avatar' => "http://lorempixel.com/800/400/",
- 'login' => null,
- )
+override or only for a single action:
+
+.. literalinclude:: fabricator/017.php
+
+Notice after the first return the fabricator stops using the overrides:
+
+.. literalinclude:: fabricator/018.php
If no second parameter is supplied then passed values will persist by default.
@@ -217,16 +137,13 @@ Test Helper
===========
Often all you will need is a one-and-done fake object for testing. The Test Helper provides
-the ``fake($model, $overrides, $persist = true)`` function to do just this::
+the ``fake($model, $overrides, $persist = true)`` function to do just this:
- helper('test');
- $user = fake('App\Models\UserModel', ['name' => 'Gerry']);
+.. literalinclude:: fabricator/019.php
-This is equivalent to::
+This is equivalent to:
- $fabricator = new Fabricator('App\Models\UserModel');
- $fabricator->setOverrides(['name' => 'Gerry']);
- $user = $fabricator->create();
+.. literalinclude:: fabricator/020.php
If you just need a fake object without saving it to the database you can pass false into the persist parameter.
@@ -240,46 +157,43 @@ example:
Your project has users and groups. In your test case you want to create various scenarios
with groups of different sizes, so you use ``Fabricator`` to create a bunch of groups.
Now you want to create fake users but don't want to assign them to a non-existant group ID.
-Your model's fake method could look like this::
-
- class UserModel
- {
- protected $table = 'users';
+Your model's fake method could look like this:
- public function fake(Generator &$faker)
- {
- return [
- 'first' => $faker->firstName,
- 'email' => $faker->email,
- 'group_id' => rand(1, Fabricator::getCount('groups')),
- ];
- }
+.. literalinclude:: fabricator/021.php
Now creating a new user will ensure it is a part of a valid group: ``$user = fake(UserModel::class);``
+Methods
+-------
+
``Fabricator`` handles the counts internally but you can also access these static methods
to assist with using them:
-**getCount(string $table): int**
+getCount(string $table): int
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Return the current value for a specific table (default: 0).
-**setCount(string $table, int $count): int**
+setCount(string $table, int $count): int
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Set the value for a specific table manually, for example if you create some test items
without using a fabricator that you still wanted factored into the final counts.
-**upCount(string $table): int**
+upCount(string $table): int
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
Increment the value for a specific table by one and return the new value. (This is what is
used internally with ``Fabricator::create()``).
-**downCount(string $table): int**
+downCount(string $table): int
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Decrement the value for a specific table by one and return the new value, for example if
you deleted a fake item but wanted to track the change.
-**resetCounts()**
+resetCounts()
+^^^^^^^^^^^^^
Resets all counts. Good idea to call this between test cases (though using
``CIUnitTestCase::$refresh = true`` does it automatically).
diff --git a/user_guide_src/source/testing/fabricator/001.php b/user_guide_src/source/testing/fabricator/001.php
new file mode 100644
index 000000000000..8fbf136032ca
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/001.php
@@ -0,0 +1,10 @@
+ 'firstName',
+ 'email' => 'email',
+ 'phone' => 'phoneNumber',
+ 'avatar' => 'imageUrl',
+];
+
+$fabricator = new Fabricator(UserModel::class, $formatters);
diff --git a/user_guide_src/source/testing/fabricator/005.php b/user_guide_src/source/testing/fabricator/005.php
new file mode 100644
index 000000000000..c61fa527125e
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/005.php
@@ -0,0 +1,32 @@
+ $faker->firstName,
+ 'email' => $faker->email,
+ 'phone' => $faker->phoneNumber,
+ 'avatar' => Faker\Provider\Image::imageUrl(800, 400),
+ 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null,
+ ];
+
+ /*
+ * Or you can return a return type object.
+
+ return new User([
+ 'first' => $faker->firstName,
+ 'email' => $faker->email,
+ 'phone' => $faker->phoneNumber,
+ 'avatar' => Faker\Provider\Image::imageUrl(800, 400),
+ 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null,
+ ]);
+
+ */
+ }
+}
diff --git a/user_guide_src/source/testing/fabricator/006.php b/user_guide_src/source/testing/fabricator/006.php
new file mode 100644
index 000000000000..56f34781622c
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/006.php
@@ -0,0 +1,13 @@
+make();
+print_r($testUser);
diff --git a/user_guide_src/source/testing/fabricator/009.php b/user_guide_src/source/testing/fabricator/009.php
new file mode 100644
index 000000000000..a82d6063213e
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/009.php
@@ -0,0 +1,9 @@
+ 'Maynard',
+ 'email' => 'king.alford@example.org',
+ 'phone' => '201-886-0269 x3767',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+];
diff --git a/user_guide_src/source/testing/fabricator/010.php b/user_guide_src/source/testing/fabricator/010.php
new file mode 100644
index 000000000000..54b795ee2f72
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/010.php
@@ -0,0 +1,3 @@
+make(10);
diff --git a/user_guide_src/source/testing/fabricator/011.php b/user_guide_src/source/testing/fabricator/011.php
new file mode 100644
index 000000000000..9b9f62fd84ee
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/011.php
@@ -0,0 +1,5 @@
+makeArray();
+$userObject = $fabricator->makeObject();
+$userEntity = $fabricator->makeObject('App\Entities\User');
diff --git a/user_guide_src/source/testing/fabricator/012.php b/user_guide_src/source/testing/fabricator/012.php
new file mode 100644
index 000000000000..98e83b610318
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/012.php
@@ -0,0 +1,12 @@
+ 1,
+ 'first' => 'Rachel',
+ 'email' => 'bradley72@gmail.com',
+ 'phone' => '741-241-2356',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+ 'created_at' => '2020-05-08 14:52:10',
+ 'updated_at' => '2020-05-08 14:52:10',
+];
diff --git a/user_guide_src/source/testing/fabricator/013.php b/user_guide_src/source/testing/fabricator/013.php
new file mode 100644
index 000000000000..6e89dd774eb9
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/013.php
@@ -0,0 +1,3 @@
+create(100);
diff --git a/user_guide_src/source/testing/fabricator/014.php b/user_guide_src/source/testing/fabricator/014.php
new file mode 100644
index 000000000000..60f0161b795f
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/014.php
@@ -0,0 +1,6 @@
+assertIsNumeric($user->id);
+$this->dontSeeInDatabase('user', ['id' => $user->id]);
diff --git a/user_guide_src/source/testing/fabricator/015.php b/user_guide_src/source/testing/fabricator/015.php
new file mode 100644
index 000000000000..277c32fa370b
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/015.php
@@ -0,0 +1,4 @@
+setOverrides(['first' => 'Bobby']);
+$bobbyUser = $fabricator->make();
diff --git a/user_guide_src/source/testing/fabricator/016.php b/user_guide_src/source/testing/fabricator/016.php
new file mode 100644
index 000000000000..8b3f9513280d
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/016.php
@@ -0,0 +1,17 @@
+ 'Bobby',
+ 'email' => 'latta.kindel@company.org',
+ 'phone' => '251-806-2169',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+];
+
+[
+ 'first' => 'Bobby',
+ 'email' => 'melissa.strike@fabricon.us',
+ 'phone' => '525-214-2656 x23546',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+];
diff --git a/user_guide_src/source/testing/fabricator/017.php b/user_guide_src/source/testing/fabricator/017.php
new file mode 100644
index 000000000000..ee79ea6f8d03
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/017.php
@@ -0,0 +1,5 @@
+setOverrides(['first' => 'Bobby'], $persist = false);
+$bobbyUser = $fabricator->make();
+$bobbyUser = $fabricator->make();
diff --git a/user_guide_src/source/testing/fabricator/018.php b/user_guide_src/source/testing/fabricator/018.php
new file mode 100644
index 000000000000..9eb1dbf0334d
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/018.php
@@ -0,0 +1,17 @@
+ 'Bobby',
+ 'email' => 'belingadon142@example.org',
+ 'phone' => '741-857-1933 x1351',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+];
+
+[
+ 'first' => 'Hans',
+ 'email' => 'hoppifur@metraxalon.com',
+ 'phone' => '487-235-7006',
+ 'avatar' => 'http://lorempixel.com/800/400/',
+ 'login' => null,
+];
diff --git a/user_guide_src/source/testing/fabricator/019.php b/user_guide_src/source/testing/fabricator/019.php
new file mode 100644
index 000000000000..4c1de5f23dd2
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/019.php
@@ -0,0 +1,4 @@
+ 'Gerry']);
diff --git a/user_guide_src/source/testing/fabricator/020.php b/user_guide_src/source/testing/fabricator/020.php
new file mode 100644
index 000000000000..385646b2ea2c
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/020.php
@@ -0,0 +1,5 @@
+setOverrides(['name' => 'Gerry']);
+$user = $fabricator->create();
diff --git a/user_guide_src/source/testing/fabricator/021.php b/user_guide_src/source/testing/fabricator/021.php
new file mode 100644
index 000000000000..236dd553d492
--- /dev/null
+++ b/user_guide_src/source/testing/fabricator/021.php
@@ -0,0 +1,17 @@
+ $faker->firstName,
+ 'email' => $faker->email,
+ 'group_id' => mt_rand(1, Fabricator::getCount('groups')),
+ ];
+ }
+}
diff --git a/user_guide_src/source/testing/feature.rst b/user_guide_src/source/testing/feature.rst
index 2a9eb96a348a..df40937ed14b 100644
--- a/user_guide_src/source/testing/feature.rst
+++ b/user_guide_src/source/testing/feature.rst
@@ -18,33 +18,8 @@ Feature testing requires that all of your test classes use the ``CodeIgniter\Tes
and ``CodeIgniter\Test\FeatureTestTrait`` traits. Since these testing tools rely on proper database
staging you must always ensure that ``parent::setUp()`` and ``parent::tearDown()``
are called if you implement your own methods.
-::
- myClassMethod();
- }
-
- protected function tearDown(): void
- {
- parent::tearDown();
-
- $this->anotherClassMethod();
- }
- }
+.. literalinclude:: feature/001.php
Requesting A Page
=================
@@ -54,25 +29,12 @@ to do this, you use the ``call()`` method. The first parameter is the HTTP metho
The second parameter is the path on your site to test. The third parameter accepts an array that is used to populate the
superglobal variables for the HTTP verb you are using. So, a method of **GET** would have the **$_GET** variable
populated, while a **post** request would have the **$_POST** array populated.
-::
-
- // Get a simple page
- $result = $this->call('get', '/');
- // Submit a form
- $result = $this->call('post', 'contact'), [
- 'name' => 'Fred Flintstone',
- 'email' => 'flintyfred@example.com'
- ]);
+.. literalinclude:: feature/002.php
-Shorthand methods for each of the HTTP verbs exist to ease typing and make things clearer::
+Shorthand methods for each of the HTTP verbs exist to ease typing and make things clearer:
- $this->get($path, $params);
- $this->post($path, $params);
- $this->put($path, $params);
- $this->patch($path, $params);
- $this->delete($path, $params);
- $this->options($path, $params);
+.. literalinclude:: feature/003.php
.. note:: The ``$params`` array does not make sense for every HTTP verb, but is included for consistency.
@@ -80,57 +42,37 @@ Setting Different Routes
------------------------
You can use a custom collection of routes by passing an array of "routes" into the ``withRoutes()`` method. This will
-override any existing routes in the system::
+override any existing routes in the system:
- $routes = [
- ['get', 'users', 'UserController::list'],
- ];
-
- $result = $this->withRoutes($routes)->get('users');
+.. literalinclude:: feature/004.php
Each of the "routes" is a 3 element array containing the HTTP verb (or "add" for all),
the URI to match, and the routing destination.
-
Setting Session Values
----------------------
You can set custom session values to use during a single test with the ``withSession()`` method. This takes an array
of key/value pairs that should exist within the ``$_SESSION`` variable when this request is made, or ``null`` to indicate
that the current values of ``$_SESSION`` should be used. This is handy for testing authentication and more.
-::
-
- $values = [
- 'logged_in' => 123,
- ];
-
- $result = $this->withSession($values)->get('admin');
- // Or...
-
- $_SESSION['logged_in'] = 123;
-
- $result = $this->withSession()->get('admin');
+.. literalinclude:: feature/005.php
Setting Headers
---------------
You can set header values with the ``withHeaders()`` method. This takes an array of key/value pairs that would be
-passed as a header into the call.::
+passed as a header into the call:
- $headers = [
- 'CONTENT_TYPE' => 'application/json',
- ];
-
- $result = $this->withHeaders($headers)->post('users');
+.. literalinclude:: feature/006.php
Bypassing Events
----------------
Events are handy to use in your application, but can be problematic during testing. Especially events that are used
-to send out emails. You can tell the system to skip any event handling with the ``skipEvents()`` method::
+to send out emails. You can tell the system to skip any event handling with the ``skipEvents()`` method:
- $result = $this->skipEvents()->post('users', $userInfo);
+.. literalinclude:: feature/007.php
Formatting The Request
-----------------------
@@ -139,13 +81,8 @@ You can set the format of your request's body using the ``withBodyFormat()`` met
`json` or `xml`. This will take the parameters passed into ``call()``, ``post()``, ``get()``... and assign them to the
body of the request in the given format. This will also set the `Content-Type` header for your request accordingly.
This is useful when testing JSON or XML API's so that you can set the request in the form that the controller will expect.
-::
-
- // If your feature test contains this:
- $result = $this->withBodyFormat('json')->post('users', $userInfo);
- // Your controller can then get the parameters passed in with:
- $userInfo = $this->request->getJson();
+.. literalinclude:: feature/008.php
Setting the Body
----------------
diff --git a/user_guide_src/source/testing/feature/001.php b/user_guide_src/source/testing/feature/001.php
new file mode 100644
index 000000000000..0ab640465052
--- /dev/null
+++ b/user_guide_src/source/testing/feature/001.php
@@ -0,0 +1,26 @@
+myClassMethod();
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ $this->anotherClassMethod();
+ }
+}
diff --git a/user_guide_src/source/testing/feature/002.php b/user_guide_src/source/testing/feature/002.php
new file mode 100644
index 000000000000..8af5fda7b7c6
--- /dev/null
+++ b/user_guide_src/source/testing/feature/002.php
@@ -0,0 +1,10 @@
+call('get', '/');
+
+// Submit a form
+$result = $this->call('post', 'contact', [
+ 'name' => 'Fred Flintstone',
+ 'email' => 'flintyfred@example.com',
+]);
diff --git a/user_guide_src/source/testing/feature/003.php b/user_guide_src/source/testing/feature/003.php
new file mode 100644
index 000000000000..1905a2dcf110
--- /dev/null
+++ b/user_guide_src/source/testing/feature/003.php
@@ -0,0 +1,8 @@
+get($path, $params);
+$this->post($path, $params);
+$this->put($path, $params);
+$this->patch($path, $params);
+$this->delete($path, $params);
+$this->options($path, $params);
diff --git a/user_guide_src/source/testing/feature/004.php b/user_guide_src/source/testing/feature/004.php
new file mode 100644
index 000000000000..4f98ced2f8d6
--- /dev/null
+++ b/user_guide_src/source/testing/feature/004.php
@@ -0,0 +1,7 @@
+withRoutes($routes)->get('users');
diff --git a/user_guide_src/source/testing/feature/005.php b/user_guide_src/source/testing/feature/005.php
new file mode 100644
index 000000000000..448e68800a4e
--- /dev/null
+++ b/user_guide_src/source/testing/feature/005.php
@@ -0,0 +1,13 @@
+ 123,
+];
+
+$result = $this->withSession($values)->get('admin');
+
+// Or...
+
+$_SESSION['logged_in'] = 123;
+
+$result = $this->withSession()->get('admin');
diff --git a/user_guide_src/source/testing/feature/006.php b/user_guide_src/source/testing/feature/006.php
new file mode 100644
index 000000000000..e3cf72b69b47
--- /dev/null
+++ b/user_guide_src/source/testing/feature/006.php
@@ -0,0 +1,7 @@
+ 'application/json',
+];
+
+$result = $this->withHeaders($headers)->post('users');
diff --git a/user_guide_src/source/testing/feature/007.php b/user_guide_src/source/testing/feature/007.php
new file mode 100644
index 000000000000..f44369c79865
--- /dev/null
+++ b/user_guide_src/source/testing/feature/007.php
@@ -0,0 +1,3 @@
+skipEvents()->post('users', $userInfo);
diff --git a/user_guide_src/source/testing/feature/008.php b/user_guide_src/source/testing/feature/008.php
new file mode 100644
index 000000000000..f5118ea42091
--- /dev/null
+++ b/user_guide_src/source/testing/feature/008.php
@@ -0,0 +1,7 @@
+withBodyFormat('json')->post('users', $userInfo);
+
+// Your controller can then get the parameters passed in with:
+$userInfo = $this->request->getJson();
diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst
index 758f6c2f3a50..967cfba7f634 100644
--- a/user_guide_src/source/testing/mocking.rst
+++ b/user_guide_src/source/testing/mocking.rst
@@ -15,9 +15,8 @@ Cache
=====
You can mock the cache with the ``mock()`` method, using the ``CacheFactory`` as its only parameter.
-::
- $mock = mock(CodeIgniter\Cache\CacheFactory::class);
+.. literalinclude:: mocking/001.php
While this returns an instance of ``CodeIgniter\Test\Mock\MockCache`` that you can use directly, it also inserts the
mock into the Service class, so any calls within your code to ``service('cache')`` or ``Config\Services::cache()`` will
@@ -31,23 +30,12 @@ Additional Methods
You can instruct the mocked cache handler to never do any caching with the ``bypass()`` method. This will emulate
using the dummy handler and ensures that your test does not rely on cached data for your tests.
-::
- $mock = mock(CodeIgniter\Cache\CacheFactory::class);
- // Never cache any items during this test.
- $mock->bypass();
+.. literalinclude:: mocking/002.php
Available Assertions
--------------------
The following new assertions are available on the mocked class for using during testing:
-::
- $mock = mock(CodeIgniter\Cache\CacheFactory::class);
-
- // Assert that a cached item named $key exists
- $mock->assertHas($key);
- // Assert that a cached item named $key exists with a value of $value
- $mock->assertHasValue($key, $value);
- // Assert that a cached item named $key does NOT exist
- $mock->assertMissing($key);
+.. literalinclude:: mocking/003.php
diff --git a/user_guide_src/source/testing/mocking/001.php b/user_guide_src/source/testing/mocking/001.php
new file mode 100644
index 000000000000..69e69239049d
--- /dev/null
+++ b/user_guide_src/source/testing/mocking/001.php
@@ -0,0 +1,3 @@
+bypass();
diff --git a/user_guide_src/source/testing/mocking/003.php b/user_guide_src/source/testing/mocking/003.php
new file mode 100644
index 000000000000..4a3babddaf54
--- /dev/null
+++ b/user_guide_src/source/testing/mocking/003.php
@@ -0,0 +1,10 @@
+assertHas($key);
+// Assert that a cached item named $key exists with a value of $value
+$mock->assertHasValue($key, $value);
+// Assert that a cached item named $key does NOT exist
+$mock->assertMissing($key);
diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst
index 4b4cdce7beed..77dbf9f3da80 100644
--- a/user_guide_src/source/testing/overview.rst
+++ b/user_guide_src/source/testing/overview.rst
@@ -3,12 +3,12 @@ Testing
#######
CodeIgniter has been built to make testing both the framework and your application as simple as possible.
-Support for ``PHPUnit`` is built in, and the framework provides a number of convenient
+Support for `PHPUnit `__ is built in, and the framework provides a number of convenient
helper methods to make testing every aspect of your application as painless as possible.
.. contents::
:local:
- :depth: 2
+ :depth: 3
*************
System Set Up
@@ -35,15 +35,18 @@ application and system directories) type the following from the command line::
This will install the correct version for your current PHP version. Once that is done, you can run all of the
tests for this project by typing::
- > ./vendor/bin/phpunit
+ > vendor/bin/phpunit
+
+If you are using Windows, use the following command::
+
+ > vendor\bin\phpunit
Phar
----
-The other option is to download the .phar file from the `PHPUnit `__ site.
+The other option is to download the .phar file from the `PHPUnit `__ site.
This is a standalone file that should be placed within your project root.
-
************************
Testing Your Application
************************
@@ -64,38 +67,13 @@ The Test Class
In order to take advantage of the additional tools provided, your tests must extend ``CIUnitTestCase``. All tests
are expected to be located in the **tests/app** directory by default.
-To test a new library, **Foo**, you would create a new file at **tests/app/Libraries/FooTest.php**::
-
- model->purgeDeleted()
- }
+.. literalinclude:: overview/005.php
Traits
------
@@ -157,152 +118,98 @@ A common way to enhance your tests is by using traits to consolidate staging acr
test cases. ``CIUnitTestCase`` will detect any class traits and look for staging methods
to run named for the trait itself. For example, if you needed to add authentication to some
of your test cases you could create an authentication trait with a set up method to fake a
-logged in user::
-
- trait AuthTrait
- {
- protected setUpAuthTrait()
- {
- $user = $this->createFakeUser();
- $this->logInUser($user);
- }
- ...
-
- class AuthenticationFeatureTest
- {
- use AuthTrait;
- ...
+logged in user:
+.. literalinclude:: overview/006.php
Additional Assertions
---------------------
``CIUnitTestCase`` provides additional unit testing assertions that you might find useful.
-**assertLogged($level, $expectedMessage)**
-
-Ensure that something you expected to be logged actually was::
-
- $config = new LoggerConfig();
- $logger = new Logger($config);
-
- ... do something that you expect a log entry from
- $logger->log('error', "That's no moon");
+assertLogged($level, $expectedMessage)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- $this->assertLogged('error', "That's no moon");
+Ensure that something you expected to be logged actually was:
-**assertEventTriggered($eventName)**
+.. literalinclude:: overview/007.php
-Ensure that an event you expected to be triggered actually was::
+assertEventTriggered($eventName)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Events::on('foo', function ($arg) use(&$result) {
- $result = $arg;
- });
+Ensure that an event you expected to be triggered actually was:
- Events::trigger('foo', 'bar');
+.. literalinclude:: overview/008.php
- $this->assertEventTriggered('foo');
+assertHeaderEmitted($header, $ignoreCase = false)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-**assertHeaderEmitted($header, $ignoreCase = false)**
+Ensure that a header or cookie was actually emitted:
-Ensure that a header or cookie was actually emitted::
+.. literalinclude:: overview/009.php
- $response->setCookie('foo', 'bar');
+.. note:: the test case with this should be `run as a separate process
+ in PHPunit `_.
- ob_start();
- $this->response->send();
- $output = ob_get_clean(); // in case you want to check the actual body
+assertHeaderNotEmitted($header, $ignoreCase = false)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- $this->assertHeaderEmitted("Set-Cookie: foo=bar");
+Ensure that a header or cookie was not emitted:
-Note: the test case with this should be `run as a separate process
-in PHPunit `_.
+.. literalinclude:: overview/010.php
-**assertHeaderNotEmitted($header, $ignoreCase = false)**
+.. note:: the test case with this should be `run as a separate process
+ in PHPunit `_.
-Ensure that a header or cookie was not emitted::
-
- $response->setCookie('foo', 'bar');
-
- ob_start();
- $this->response->send();
- $output = ob_get_clean(); // in case you want to check the actual body
-
- $this->assertHeaderNotEmitted("Set-Cookie: banana");
-
-Note: the test case with this should be `run as a separate process
-in PHPunit `_.
-
-**assertCloseEnough($expected, $actual, $message = '', $tolerance = 1)**
+assertCloseEnough($expected, $actual, $message = '', $tolerance = 1)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For extended execution time testing, tests that the absolute difference
-between expected and actual time is within the prescribed tolerance.::
+between expected and actual time is within the prescribed tolerance:
- $timer = new Timer();
- $timer->start('longjohn', strtotime('-11 minutes'));
- $this->assertCloseEnough(11 * 60, $timer->getElapsedTime('longjohn'));
+.. literalinclude:: overview/011.php
The above test will allow the actual time to be either 660 or 661 seconds.
-**assertCloseEnoughString($expected, $actual, $message = '', $tolerance = 1)**
+assertCloseEnoughString($expected, $actual, $message = '', $tolerance = 1)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For extended execution time testing, tests that the absolute difference
-between expected and actual time, formatted as strings, is within the prescribed tolerance.::
+between expected and actual time, formatted as strings, is within the prescribed tolerance:
- $timer = new Timer();
- $timer->start('longjohn', strtotime('-11 minutes'));
- $this->assertCloseEnoughString(11 * 60, $timer->getElapsedTime('longjohn'));
+.. literalinclude:: overview/012.php
The above test will allow the actual time to be either 660 or 661 seconds.
-
Accessing Protected/Private Properties
--------------------------------------
When testing, you can use the following setter and getter methods to access protected and private methods and
properties in the classes that you are testing.
-**getPrivateMethodInvoker($instance, $method)**
+getPrivateMethodInvoker($instance, $method)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Enables you to call private methods from outside the class. This returns a function that can be called. The first
parameter is an instance of the class to test. The second parameter is the name of the method you want to call.
-::
-
- // Create an instance of the class to test
- $obj = new Foo();
-
- // Get the invoker for the 'privateMethod' method.
- $method = $this->getPrivateMethodInvoker($obj, 'privateMethod');
-
- // Test the results
- $this->assertEquals('bar', $method('param1', 'param2'));
+.. literalinclude:: overview/013.php
-**getPrivateProperty($instance, $property)**
+getPrivateProperty($instance, $property)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves the value of a private/protected class property from an instance of a class. The first parameter is an
instance of the class to test. The second parameter is the name of the property.
-::
+.. literalinclude:: overview/014.php
- // Create an instance of the class to test
- $obj = new Foo();
-
- // Test the value
- $this->assertEquals('bar', $this->getPrivateProperty($obj, 'baz'));
-
-**setPrivateProperty($instance, $property, $value)**
+setPrivateProperty($instance, $property, $value)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Set a protected value within a class instance. The first parameter is an instance of the class to test. The second
-parameter is the name of the property to set the value of. The third parameter is the value to set it to::
-
- // Create an instance of the class to test
- $obj = new Foo();
-
- // Set the value
- $this->setPrivateProperty($obj, 'baz', 'oops!');
+parameter is the name of the property to set the value of. The third parameter is the value to set it to:
- // Do normal testing...
+.. literalinclude:: overview/015.php
Mocking Services
================
@@ -312,30 +219,26 @@ your tests to only the code in question, while simulating various responses from
true when testing controllers and other integration testing. The **Services** class provides the following methods
to simplify this.
-**injectMock()**
+Services::injectMock()
+----------------------
This method allows you to define the exact instance that will be returned by the Services class. You can use this to
set properties of a service so that it behaves in a certain way, or replace a service with a mocked class.
-::
- public function testSomething()
- {
- $curlrequest = $this->getMockBuilder('CodeIgniter\HTTP\CURLRequest')
- ->setMethods(['request'])
- ->getMock();
- Services::injectMock('curlrequest', $curlrequest);
-
- // Do normal testing here....
- }
+.. literalinclude:: overview/016.php
The first parameter is the service that you are replacing. The name must match the function name in the Services
class exactly. The second parameter is the instance to replace it with.
-**reset()**
+Services::reset()
+-----------------
Removes all mocked classes from the Services class, bringing it back to its original state.
-**resetSingle(string $name)**
+You can also use the ``$this->resetServices()`` method that ``CIUnitTestCase`` provides.
+
+Services::resetSingle(string $name)
+-----------------------------------
Removes any mock and shared instances for a single service, by its name.
@@ -345,17 +248,11 @@ Mocking Factory Instances
=========================
Similar to Services, you may find yourself needing to supply a pre-configured class instance
-during testing that will be used with ``Factories``. Use the same ``injectMock()`` and ``reset()``
+during testing that will be used with ``Factories``. Use the same ``Factories::injectMock()`` and ``Factories::reset()``
static methods like **Services**, but they take an additional preceding parameter for the
-component name::
+component name:
- protected function setUp()
- {
- parent::setUp();
-
- $model = new MockUserModel();
- Factories::injectMock('models', 'App\Models\UserModel', $model);
- }
+.. literalinclude:: overview/017.php
.. note:: All component Factories are reset by default between each test. Modify your test case's ``$setUpMethods`` if you need instances to persist.
@@ -367,22 +264,6 @@ Stream Filters
You may need to test things that are difficult to test. Sometimes, capturing a stream, like PHP's own STDOUT, or STDERR,
might be helpful. The ``CITestStreamFilter`` helps you capture the output from the stream of your choice.
-An example demonstrating this inside one of your test cases::
-
- public function setUp()
- {
- CITestStreamFilter::$buffer = '';
- $this->stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter');
- }
-
- public function tearDown()
- {
- stream_filter_remove($this->stream_filter);
- }
-
- public function testSomeOutput()
- {
- CLI::write('first.');
- $expected = "first.\n";
- $this->assertSame($expected, CITestStreamFilter::$buffer);
- }
+An example demonstrating this inside one of your test cases:
+
+.. literalinclude:: overview/018.php
diff --git a/user_guide_src/source/testing/overview/001.php b/user_guide_src/source/testing/overview/001.php
new file mode 100644
index 000000000000..6dac95d2b671
--- /dev/null
+++ b/user_guide_src/source/testing/overview/001.php
@@ -0,0 +1,13 @@
+model->purgeDeleted();
+ }
+}
diff --git a/user_guide_src/source/testing/overview/006.php b/user_guide_src/source/testing/overview/006.php
new file mode 100644
index 000000000000..6193eb88bf50
--- /dev/null
+++ b/user_guide_src/source/testing/overview/006.php
@@ -0,0 +1,21 @@
+createFakeUser();
+ $this->logInUser($user);
+ }
+
+ // ...
+}
+
+use CodeIgniter\Test\CIUnitTestCase;
+
+final class AuthenticationFeatureTest extends CIUnitTestCase
+{
+ use AuthTrait;
+
+ // ...
+}
diff --git a/user_guide_src/source/testing/overview/007.php b/user_guide_src/source/testing/overview/007.php
new file mode 100644
index 000000000000..55077ed36818
--- /dev/null
+++ b/user_guide_src/source/testing/overview/007.php
@@ -0,0 +1,9 @@
+log('error', "That's no moon");
+
+$this->assertLogged('error', "That's no moon");
diff --git a/user_guide_src/source/testing/overview/008.php b/user_guide_src/source/testing/overview/008.php
new file mode 100644
index 000000000000..5d69c01c16d5
--- /dev/null
+++ b/user_guide_src/source/testing/overview/008.php
@@ -0,0 +1,9 @@
+assertEventTriggered('foo');
diff --git a/user_guide_src/source/testing/overview/009.php b/user_guide_src/source/testing/overview/009.php
new file mode 100644
index 000000000000..506d75a8d433
--- /dev/null
+++ b/user_guide_src/source/testing/overview/009.php
@@ -0,0 +1,9 @@
+setCookie('foo', 'bar');
+
+ob_start();
+$this->response->send();
+$output = ob_get_clean(); // in case you want to check the actual body
+
+$this->assertHeaderEmitted('Set-Cookie: foo=bar');
diff --git a/user_guide_src/source/testing/overview/010.php b/user_guide_src/source/testing/overview/010.php
new file mode 100644
index 000000000000..10e9d4a0d31b
--- /dev/null
+++ b/user_guide_src/source/testing/overview/010.php
@@ -0,0 +1,9 @@
+setCookie('foo', 'bar');
+
+ob_start();
+$this->response->send();
+$output = ob_get_clean(); // in case you want to check the actual body
+
+$this->assertHeaderNotEmitted('Set-Cookie: banana');
diff --git a/user_guide_src/source/testing/overview/011.php b/user_guide_src/source/testing/overview/011.php
new file mode 100644
index 000000000000..c9b85aef7f87
--- /dev/null
+++ b/user_guide_src/source/testing/overview/011.php
@@ -0,0 +1,5 @@
+start('longjohn', strtotime('-11 minutes'));
+$this->assertCloseEnough(11 * 60, $timer->getElapsedTime('longjohn'));
diff --git a/user_guide_src/source/testing/overview/012.php b/user_guide_src/source/testing/overview/012.php
new file mode 100644
index 000000000000..1c03733868e4
--- /dev/null
+++ b/user_guide_src/source/testing/overview/012.php
@@ -0,0 +1,5 @@
+start('longjohn', strtotime('-11 minutes'));
+$this->assertCloseEnoughString(11 * 60, $timer->getElapsedTime('longjohn'));
diff --git a/user_guide_src/source/testing/overview/013.php b/user_guide_src/source/testing/overview/013.php
new file mode 100644
index 000000000000..f228dcf28963
--- /dev/null
+++ b/user_guide_src/source/testing/overview/013.php
@@ -0,0 +1,10 @@
+getPrivateMethodInvoker($obj, 'privateMethod');
+
+// Test the results
+$this->assertEquals('bar', $method('param1', 'param2'));
diff --git a/user_guide_src/source/testing/overview/014.php b/user_guide_src/source/testing/overview/014.php
new file mode 100644
index 000000000000..1befb710253e
--- /dev/null
+++ b/user_guide_src/source/testing/overview/014.php
@@ -0,0 +1,7 @@
+assertEquals('bar', $this->getPrivateProperty($obj, 'baz'));
diff --git a/user_guide_src/source/testing/overview/015.php b/user_guide_src/source/testing/overview/015.php
new file mode 100644
index 000000000000..c2e815953dd9
--- /dev/null
+++ b/user_guide_src/source/testing/overview/015.php
@@ -0,0 +1,9 @@
+setPrivateProperty($obj, 'baz', 'oops!');
+
+// Do normal testing...
diff --git a/user_guide_src/source/testing/overview/016.php b/user_guide_src/source/testing/overview/016.php
new file mode 100644
index 000000000000..d3a769d8379c
--- /dev/null
+++ b/user_guide_src/source/testing/overview/016.php
@@ -0,0 +1,17 @@
+getMockBuilder('CodeIgniter\HTTP\CURLRequest')
+ ->setMethods(['request'])
+ ->getMock();
+ Services::injectMock('curlrequest', $curlrequest);
+
+ // Do normal testing here....
+ }
+}
diff --git a/user_guide_src/source/testing/overview/017.php b/user_guide_src/source/testing/overview/017.php
new file mode 100644
index 000000000000..c31ac96eb459
--- /dev/null
+++ b/user_guide_src/source/testing/overview/017.php
@@ -0,0 +1,15 @@
+stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter');
+ }
+
+ protected function tearDown(): void
+ {
+ stream_filter_remove($this->stream_filter);
+ }
+
+ public function testSomeOutput(): void
+ {
+ CLI::write('first.');
+
+ $expected = "first.\n";
+ $this->assertSame($expected, CITestStreamFilter::$buffer);
+ }
+}
diff --git a/user_guide_src/source/testing/response.rst b/user_guide_src/source/testing/response.rst
index 6c22e7e9cbdb..c28fd46bbb81 100644
--- a/user_guide_src/source/testing/response.rst
+++ b/user_guide_src/source/testing/response.rst
@@ -5,331 +5,285 @@ Testing Responses
The ``TestResponse`` class provides a number of helpful functions for parsing and testing responses
from your test cases. Usually a ``TestResponse`` will be provided for you as a result of your
:doc:`Controller Tests ` or :doc:`HTTP Feature Tests `, but you can always
-create your own directly using any ``ResponseInterface``::
+create your own directly using any ``ResponseInterface``:
- $result = new \CodeIgniter\Test\TestResponse($response);
- $result->assertOK();
+.. literalinclude:: response/001.php
.. contents::
:local:
:depth: 2
Testing the Response
-====================
+********************
Whether you have received a ``TestResponse`` as a result of your tests or created one yourself,
there are a number of new assertions that you can use in your tests.
Accessing Request/Response
---------------------------
+==========================
-**request()**
+request()
+---------
-You can access directly the Request object, if it was set during testing::
+You can access directly the Request object, if it was set during testing:
- $request = $results->request();
+.. literalinclude:: response/002.php
-**response()**
+response()
+----------
-This allows you direct access to the response object::
+This allows you direct access to the response object:
- $response = $results->response();
+.. literalinclude:: response/003.php
Checking Response Status
-------------------------
+========================
-**isOK()**
+isOK()
+------
Returns a boolean true/false based on whether the response is perceived to be "ok". This is primarily determined by
a response status code in the 200 or 300's. An empty body is not considered valid, unless in redirects.
-::
- if ($result->isOK()) {
- ...
- }
+.. literalinclude:: response/004.php
-**assertOK()**
+assertOK()
+----------
-This assertion simply uses the **isOK()** method to test a response. **assertNotOK** is the inverse of this assertion.
-::
+This assertion simply uses the ``isOK()`` method to test a response. ``assertNotOK()`` is the inverse of this assertion.
- $result->assertOK();
+.. literalinclude:: response/005.php
-**isRedirect()**
+isRedirect()
+------------
Returns a boolean true/false based on whether the response is a redirected response.
-::
- if ($result->isRedirect()) {
- ...
- }
+.. literalinclude:: response/006.php
-**assertRedirect()**
+assertRedirect()
+----------------
-Asserts that the Response is an instance of RedirectResponse. **assertNotRedirect** is the inverse of this assertion.
-::
+Asserts that the Response is an instance of RedirectResponse. ``assertNotRedirect()`` is the inverse of this assertion.
- $result->assertRedirect();
+.. literalinclude:: response/007.php
-**assertRedirectTo()**
+assertRedirectTo()
+------------------
Asserts that the Response is an instance of RedirectResponse and the destination
matches the uri given.
-::
- $result->assertRedirectTo('foo/bar');
+.. literalinclude:: response/008.php
-**getRedirectUrl()**
+getRedirectUrl()
+----------------
Returns the URL set for a RedirectResponse, or null for failure.
-::
- $url = $result->getRedirectUrl();
- $this->assertEquals(site_url('foo/bar'), $url);
+.. literalinclude:: response/009.php
-**assertStatus(int $code)**
+assertStatus(int $code)
+-----------------------
Asserts that the HTTP status code returned matches $code.
-::
-
- $result->assertStatus(403);
+.. literalinclude:: response/010.php
Session Assertions
-------------------
+==================
-**assertSessionHas(string $key, $value = null)**
+assertSessionHas(string $key, $value = null)
+--------------------------------------------
Asserts that a value exists in the resulting session. If $value is passed, will also assert that the variable's value
matches what was specified.
-::
- $result->assertSessionHas('logged_in', 123);
+.. literalinclude:: response/011.php
-**assertSessionMissing(string $key)**
+assertSessionMissing(string $key)
+---------------------------------
Asserts that the resulting session does not include the specified $key.
-::
-
- $result->assertSessionMissin('logged_in');
+.. literalinclude:: response/012.php
Header Assertions
------------------
+=================
-**assertHeader(string $key, $value = null)**
+assertHeader(string $key, $value = null)
+----------------------------------------
-Asserts that a header named **$key** exists in the response. If **$value** is not empty, will also assert that
+Asserts that a header named ``$key`` exists in the response. If ``$value`` is not empty, will also assert that
the values match.
-::
-
- $result->assertHeader('Content-Type', 'text/html');
-**assertHeaderMissing(string $key)**
+.. literalinclude:: response/013.php
-Asserts that a header name **$key** does not exist in the response.
-::
+assertHeaderMissing(string $key)
+--------------------------------
- $result->assertHeader('Accepts');
+Asserts that a header name ``$key`` does not exist in the response.
+.. literalinclude:: response/014.php
Cookie Assertions
------------------
+=================
-**assertCookie(string $key, $value = null, string $prefix = '')**
+assertCookie(string $key, $value = null, string $prefix = '')
+-------------------------------------------------------------
-Asserts that a cookie named **$key** exists in the response. If **$value** is not empty, will also assert that
+Asserts that a cookie named ``$key`` exists in the response. If ``$value`` is not empty, will also assert that
the values match. You can set the cookie prefix, if needed, by passing it in as the third parameter.
-::
- $result->assertCookie('foo', 'bar');
+.. literalinclude:: response/015.php
-**assertCookieMissing(string $key)**
+assertCookieMissing(string $key)
+--------------------------------
-Asserts that a cookie named **$key** does not exist in the response.
-::
+Asserts that a cookie named ``$key`` does not exist in the response.
- $result->assertCookieMissing('ci_session');
+.. literalinclude:: response/016.php
-**assertCookieExpired(string $key, string $prefix = '')**
+assertCookieExpired(string $key, string $prefix = '')
+-----------------------------------------------------
-Asserts that a cookie named **$key** exists, but has expired. You can set the cookie prefix, if needed, by passing it
+Asserts that a cookie named ``$key`` exists, but has expired. You can set the cookie prefix, if needed, by passing it
in as the second parameter.
-::
- $result->assertCookieExpired('foo');
+.. literalinclude:: response/017.php
DOM Helpers
------------
+===========
The response you get back contains a number of helper methods to inspect the HTML output within the response. These
are useful for using within assertions in your tests.
-The **see()** method checks the text on the page to see if it exists either by itself, or more specifically within
-a tag, as specified by type, class, or id::
+see()
+-----
+
+The ``see()`` method checks the text on the page to see if it exists either by itself, or more specifically within
+a tag, as specified by type, class, or id:
+
+.. literalinclude:: response/018.php
+
+The ``dontSee()`` method is the exact opposite:
- // Check that "Hello World" is on the page
- $results->see('Hello World');
- // Check that "Hello World" is within an h1 tag
- $results->see('Hello World', 'h1');
- // Check that "Hello World" is within an element with the "notice" class
- $results->see('Hello World', '.notice');
- // Check that "Hello World" is within an element with id of "title"
- $results->see('Hellow World', '#title');
+.. literalinclude:: response/019.php
-The **dontSee()** method is the exact opposite::
+seeElement()
+------------
- // Checks that "Hello World" does NOT exist on the page
- $results->dontSee('Hello World');
- // Checks that "Hellow World" does NOT exist within any h1 tag
- $results->dontSee('Hello World', 'h1');
+The ``seeElement()`` and ``dontSeeElement()`` are very similar to the previous methods, but do not look at the
+values of the elements. Instead, they simply check that the elements exist on the page:
-The **seeElement()** and **dontSeeElement()** are very similar to the previous methods, but do not look at the
-values of the elements. Instead, they simply check that the elements exist on the page::
+.. literalinclude:: response/020.php
- // Check that an element with class 'notice' exists
- $results->seeElement('.notice');
- // Check that an element with id 'title' exists
- $results->seeElement('#title')
- // Verify that an element with id 'title' does NOT exist
- $results->dontSeeElement('#title');
+seeLink()
+---------
-You can use **seeLink()** to ensure that a link appears on the page with the specified text::
+You can use ``seeLink()`` to ensure that a link appears on the page with the specified text:
- // Check that a link exists with 'Upgrade Account' as the text::
- $results->seeLink('Upgrade Account');
- // Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell'
- $results->seeLink('Upgrade Account', '.upsell');
+.. literalinclude:: response/021.php
-The **seeInField()** method checks for any input tags exist with the name and value::
+seeInField()
+------------
- // Check that an input exists named 'user' with the value 'John Snow'
- $results->seeInField('user', 'John Snow');
- // Check a multi-dimensional input
- $results->seeInField('user[name]', 'John Snow');
+The ``seeInField()`` method checks for any input tags exist with the name and value:
-Finally, you can check if a checkbox exists and is checked with the **seeCheckboxIsChecked()** method::
+.. literalinclude:: response/022.php
- // Check if checkbox is checked with class of 'foo'
- $results->seeCheckboxIsChecked('.foo');
- // Check if checkbox with id of 'bar' is checked
- $results->seeCheckboxIsChecked('#bar');
+seeCheckboxIsChecked()
+----------------------
+
+Finally, you can check if a checkbox exists and is checked with the ``seeCheckboxIsChecked()`` method:
+
+.. literalinclude:: response/023.php
DOM Assertions
---------------
+==============
You can perform tests to see if specific elements/text/etc exist with the body of the response with the following
assertions.
-**assertSee(string $search = null, string $element = null)**
+assertSee(string $search = null, string $element = null)
+--------------------------------------------------------
Asserts that text/HTML is on the page, either by itself or - more specifically - within
-a tag, as specified by type, class, or id::
-
- // Check that "Hello World" is on the page
- $result->assertSee('Hello World');
- // Check that "Hello World" is within an h1 tag
- $result->assertSee('Hello World', 'h1');
- // Check that "Hello World" is within an element with the "notice" class
- $result->assertSee('Hello World', '.notice');
- // Check that "Hello World" is within an element with id of "title"
- $result->assertSee('Hellow World', '#title');
+a tag, as specified by type, class, or id:
+.. literalinclude:: response/024.php
-**assertDontSee(string $search = null, string $element = null)**
+assertDontSee(string $search = null, string $element = null)
+------------------------------------------------------------
-Asserts the exact opposite of the **assertSee()** method::
+Asserts the exact opposite of the ``assertSee()`` method:
- // Checks that "Hello World" does NOT exist on the page
- $results->dontSee('Hello World');
- // Checks that "Hello World" does NOT exist within any h1 tag
- $results->dontSee('Hello World', 'h1');
+.. literalinclude:: response/025.php
-**assertSeeElement(string $search)**
+assertSeeElement(string $search)
+--------------------------------
-Similar to **assertSee()**, however this only checks for an existing element. It does not check for specific text::
+Similar to ``assertSee()``, however this only checks for an existing element. It does not check for specific text:
- // Check that an element with class 'notice' exists
- $results->seeElement('.notice');
- // Check that an element with id 'title' exists
- $results->seeElement('#title')
+.. literalinclude:: response/026.php
-**assertDontSeeElement(string $search)**
+assertDontSeeElement(string $search)
+------------------------------------
-Similar to **assertSee()**, however this only checks for an existing element that is missing. It does not check for
-specific text::
+Similar to ``assertSee()``, however this only checks for an existing element that is missing. It does not check for
+specific text:
- // Verify that an element with id 'title' does NOT exist
- $results->dontSeeElement('#title');
+.. literalinclude:: response/027.php
-**assertSeeLink(string $text, string $details=null)**
+assertSeeLink(string $text, string $details = null)
+---------------------------------------------------
-Asserts that an anchor tag is found with matching **$text** as the body of the tag::
+Asserts that an anchor tag is found with matching ``$text`` as the body of the tag:
- // Check that a link exists with 'Upgrade Account' as the text::
- $results->seeLink('Upgrade Account');
- // Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell'
- $results->seeLink('Upgrade Account', '.upsell');
+.. literalinclude:: response/028.php
-**assertSeeInField(string $field, string $value=null)**
+assertSeeInField(string $field, string $value = null)
+-----------------------------------------------------
-Asserts that an input tag exists with the name and value::
-
- // Check that an input exists named 'user' with the value 'John Snow'
- $results->assertSeeInField('user', 'John Snow');
- // Check a multi-dimensional input
- $results->assertSeeInField('user[name]', 'John Snow');
+Asserts that an input tag exists with the name and value:
+.. literalinclude:: response/029.php
Working With JSON
------------------
+=================
Responses will frequently contain JSON responses, especially when working with API methods. The following methods
can help to test the responses.
-**getJSON()**
-
-This method will return the body of the response as a JSON string::
+getJSON()
+---------
- // Response body is this:
- ['foo' => 'bar']
+This method will return the body of the response as a JSON string:
- $json = $result->getJSON();
+.. literalinclude:: response/030.php
- // $json is this:
- {
- "foo": "bar"
- }
+You can use this method to determine if ``$response`` actually holds JSON content:
-You can use this method to determine if ``$response`` actually holds JSON content::
-
- // Verify the response is JSON
- $this->assertTrue($result->getJSON() !== false)
+.. literalinclude:: response/031.php
.. note:: Be aware that the JSON string will be pretty-printed in the result.
-**assertJSONFragment(array $fragment)**
+assertJSONFragment(array $fragment)
+-----------------------------------
Asserts that $fragment is found within the JSON response. It does not need to match the entire JSON value.
-::
-
- // Response body is this:
- [
- 'config' => ['key-a', 'key-b'],
- ]
-
- // Is true
- $result->assertJSONFragment(['config' => ['key-a']]);
+.. literalinclude:: response/032.php
-**assertJSONExact($test)**
-
-Similar to **assertJSONFragment()**, but checks the entire JSON response to ensure exact matches.
+assertJSONExact($test)
+----------------------
+Similar to ``assertJSONFragment()``, but checks the entire JSON response to ensure exact matches.
Working With XML
-----------------
+================
-**getXML()**
+getXML()
+--------
If your application returns XML, you can retrieve it through this method.
diff --git a/user_guide_src/source/testing/response/001.php b/user_guide_src/source/testing/response/001.php
new file mode 100644
index 000000000000..0e37cfec37ba
--- /dev/null
+++ b/user_guide_src/source/testing/response/001.php
@@ -0,0 +1,4 @@
+assertOK();
diff --git a/user_guide_src/source/testing/response/002.php b/user_guide_src/source/testing/response/002.php
new file mode 100644
index 000000000000..a835659904db
--- /dev/null
+++ b/user_guide_src/source/testing/response/002.php
@@ -0,0 +1,3 @@
+request();
diff --git a/user_guide_src/source/testing/response/003.php b/user_guide_src/source/testing/response/003.php
new file mode 100644
index 000000000000..c0cb73899f30
--- /dev/null
+++ b/user_guide_src/source/testing/response/003.php
@@ -0,0 +1,3 @@
+response();
diff --git a/user_guide_src/source/testing/response/004.php b/user_guide_src/source/testing/response/004.php
new file mode 100644
index 000000000000..d3ccbf4f4d12
--- /dev/null
+++ b/user_guide_src/source/testing/response/004.php
@@ -0,0 +1,5 @@
+isOK()) {
+ // ...
+}
diff --git a/user_guide_src/source/testing/response/005.php b/user_guide_src/source/testing/response/005.php
new file mode 100644
index 000000000000..9c01964bc050
--- /dev/null
+++ b/user_guide_src/source/testing/response/005.php
@@ -0,0 +1,3 @@
+assertOK();
diff --git a/user_guide_src/source/testing/response/006.php b/user_guide_src/source/testing/response/006.php
new file mode 100644
index 000000000000..dcca55a4938b
--- /dev/null
+++ b/user_guide_src/source/testing/response/006.php
@@ -0,0 +1,5 @@
+isRedirect()) {
+ // ...
+}
diff --git a/user_guide_src/source/testing/response/007.php b/user_guide_src/source/testing/response/007.php
new file mode 100644
index 000000000000..38b43a99b200
--- /dev/null
+++ b/user_guide_src/source/testing/response/007.php
@@ -0,0 +1,3 @@
+assertRedirect();
diff --git a/user_guide_src/source/testing/response/008.php b/user_guide_src/source/testing/response/008.php
new file mode 100644
index 000000000000..62b8da3db6af
--- /dev/null
+++ b/user_guide_src/source/testing/response/008.php
@@ -0,0 +1,3 @@
+assertRedirectTo('foo/bar');
diff --git a/user_guide_src/source/testing/response/009.php b/user_guide_src/source/testing/response/009.php
new file mode 100644
index 000000000000..ba58bc23bb9e
--- /dev/null
+++ b/user_guide_src/source/testing/response/009.php
@@ -0,0 +1,4 @@
+getRedirectUrl();
+$this->assertEquals(site_url('foo/bar'), $url);
diff --git a/user_guide_src/source/testing/response/010.php b/user_guide_src/source/testing/response/010.php
new file mode 100644
index 000000000000..70faf34078e1
--- /dev/null
+++ b/user_guide_src/source/testing/response/010.php
@@ -0,0 +1,3 @@
+assertStatus(403);
diff --git a/user_guide_src/source/testing/response/011.php b/user_guide_src/source/testing/response/011.php
new file mode 100644
index 000000000000..e9fccd725002
--- /dev/null
+++ b/user_guide_src/source/testing/response/011.php
@@ -0,0 +1,3 @@
+assertSessionHas('logged_in', 123);
diff --git a/user_guide_src/source/testing/response/012.php b/user_guide_src/source/testing/response/012.php
new file mode 100644
index 000000000000..671330ac32a4
--- /dev/null
+++ b/user_guide_src/source/testing/response/012.php
@@ -0,0 +1,3 @@
+assertSessionMissin('logged_in');
diff --git a/user_guide_src/source/testing/response/013.php b/user_guide_src/source/testing/response/013.php
new file mode 100644
index 000000000000..761b39943842
--- /dev/null
+++ b/user_guide_src/source/testing/response/013.php
@@ -0,0 +1,3 @@
+assertHeader('Content-Type', 'text/html');
diff --git a/user_guide_src/source/testing/response/014.php b/user_guide_src/source/testing/response/014.php
new file mode 100644
index 000000000000..109ce46d1097
--- /dev/null
+++ b/user_guide_src/source/testing/response/014.php
@@ -0,0 +1,3 @@
+assertHeader('Accepts');
diff --git a/user_guide_src/source/testing/response/015.php b/user_guide_src/source/testing/response/015.php
new file mode 100644
index 000000000000..ded077e66a9b
--- /dev/null
+++ b/user_guide_src/source/testing/response/015.php
@@ -0,0 +1,3 @@
+assertCookie('foo', 'bar');
diff --git a/user_guide_src/source/testing/response/016.php b/user_guide_src/source/testing/response/016.php
new file mode 100644
index 000000000000..3aa73f14899a
--- /dev/null
+++ b/user_guide_src/source/testing/response/016.php
@@ -0,0 +1,3 @@
+assertCookieMissing('ci_session');
diff --git a/user_guide_src/source/testing/response/017.php b/user_guide_src/source/testing/response/017.php
new file mode 100644
index 000000000000..5ce2b3e2ab9f
--- /dev/null
+++ b/user_guide_src/source/testing/response/017.php
@@ -0,0 +1,3 @@
+assertCookieExpired('foo');
diff --git a/user_guide_src/source/testing/response/018.php b/user_guide_src/source/testing/response/018.php
new file mode 100644
index 000000000000..e6860f412e08
--- /dev/null
+++ b/user_guide_src/source/testing/response/018.php
@@ -0,0 +1,10 @@
+see('Hello World');
+// Check that "Hello World" is within an h1 tag
+$results->see('Hello World', 'h1');
+// Check that "Hello World" is within an element with the "notice" class
+$results->see('Hello World', '.notice');
+// Check that "Hello World" is within an element with id of "title"
+$results->see('Hellow World', '#title');
diff --git a/user_guide_src/source/testing/response/019.php b/user_guide_src/source/testing/response/019.php
new file mode 100644
index 000000000000..f96493b52e91
--- /dev/null
+++ b/user_guide_src/source/testing/response/019.php
@@ -0,0 +1,6 @@
+dontSee('Hello World');
+// Checks that "Hellow World" does NOT exist within any h1 tag
+$results->dontSee('Hello World', 'h1');
diff --git a/user_guide_src/source/testing/response/020.php b/user_guide_src/source/testing/response/020.php
new file mode 100644
index 000000000000..8b716717b696
--- /dev/null
+++ b/user_guide_src/source/testing/response/020.php
@@ -0,0 +1,8 @@
+seeElement('.notice');
+// Check that an element with id 'title' exists
+$results->seeElement('#title');
+// Verify that an element with id 'title' does NOT exist
+$results->dontSeeElement('#title');
diff --git a/user_guide_src/source/testing/response/021.php b/user_guide_src/source/testing/response/021.php
new file mode 100644
index 000000000000..bc74e3ad9124
--- /dev/null
+++ b/user_guide_src/source/testing/response/021.php
@@ -0,0 +1,6 @@
+seeLink('Upgrade Account');
+// Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell'
+$results->seeLink('Upgrade Account', '.upsell');
diff --git a/user_guide_src/source/testing/response/022.php b/user_guide_src/source/testing/response/022.php
new file mode 100644
index 000000000000..9bb549c9db5f
--- /dev/null
+++ b/user_guide_src/source/testing/response/022.php
@@ -0,0 +1,6 @@
+seeInField('user', 'John Snow');
+// Check a multi-dimensional input
+$results->seeInField('user[name]', 'John Snow');
diff --git a/user_guide_src/source/testing/response/023.php b/user_guide_src/source/testing/response/023.php
new file mode 100644
index 000000000000..3378144f6eb7
--- /dev/null
+++ b/user_guide_src/source/testing/response/023.php
@@ -0,0 +1,6 @@
+seeCheckboxIsChecked('.foo');
+// Check if checkbox with id of 'bar' is checked
+$results->seeCheckboxIsChecked('#bar');
diff --git a/user_guide_src/source/testing/response/024.php b/user_guide_src/source/testing/response/024.php
new file mode 100644
index 000000000000..021910b4e3f7
--- /dev/null
+++ b/user_guide_src/source/testing/response/024.php
@@ -0,0 +1,10 @@
+assertSee('Hello World');
+// Check that "Hello World" is within an h1 tag
+$result->assertSee('Hello World', 'h1');
+// Check that "Hello World" is within an element with the "notice" class
+$result->assertSee('Hello World', '.notice');
+// Check that "Hello World" is within an element with id of "title"
+$result->assertSee('Hellow World', '#title');
diff --git a/user_guide_src/source/testing/response/025.php b/user_guide_src/source/testing/response/025.php
new file mode 100644
index 000000000000..b255f4155ac5
--- /dev/null
+++ b/user_guide_src/source/testing/response/025.php
@@ -0,0 +1,6 @@
+dontSee('Hello World');
+// Checks that "Hello World" does NOT exist within any h1 tag
+$results->dontSee('Hello World', 'h1');
diff --git a/user_guide_src/source/testing/response/026.php b/user_guide_src/source/testing/response/026.php
new file mode 100644
index 000000000000..4afd20161b23
--- /dev/null
+++ b/user_guide_src/source/testing/response/026.php
@@ -0,0 +1,6 @@
+seeElement('.notice');
+// Check that an element with id 'title' exists
+$results->seeElement('#title');
diff --git a/user_guide_src/source/testing/response/027.php b/user_guide_src/source/testing/response/027.php
new file mode 100644
index 000000000000..9780489133ee
--- /dev/null
+++ b/user_guide_src/source/testing/response/027.php
@@ -0,0 +1,4 @@
+dontSeeElement('#title');
diff --git a/user_guide_src/source/testing/response/028.php b/user_guide_src/source/testing/response/028.php
new file mode 100644
index 000000000000..bc74e3ad9124
--- /dev/null
+++ b/user_guide_src/source/testing/response/028.php
@@ -0,0 +1,6 @@
+seeLink('Upgrade Account');
+// Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell'
+$results->seeLink('Upgrade Account', '.upsell');
diff --git a/user_guide_src/source/testing/response/029.php b/user_guide_src/source/testing/response/029.php
new file mode 100644
index 000000000000..1ae7910e9776
--- /dev/null
+++ b/user_guide_src/source/testing/response/029.php
@@ -0,0 +1,6 @@
+assertSeeInField('user', 'John Snow');
+// Check a multi-dimensional input
+$results->assertSeeInField('user[name]', 'John Snow');
diff --git a/user_guide_src/source/testing/response/030.php b/user_guide_src/source/testing/response/030.php
new file mode 100644
index 000000000000..47e480c6081f
--- /dev/null
+++ b/user_guide_src/source/testing/response/030.php
@@ -0,0 +1,14 @@
+ 'bar']
+ */
+
+$json = $result->getJSON();
+/*
+ * $json is this:
+ * {
+ * "foo": "bar"
+ * }
+`*/
diff --git a/user_guide_src/source/testing/response/031.php b/user_guide_src/source/testing/response/031.php
new file mode 100644
index 000000000000..6e746800e034
--- /dev/null
+++ b/user_guide_src/source/testing/response/031.php
@@ -0,0 +1,4 @@
+assertTrue($result->getJSON() !== false);
diff --git a/user_guide_src/source/testing/response/032.php b/user_guide_src/source/testing/response/032.php
new file mode 100644
index 000000000000..14f13f1942f5
--- /dev/null
+++ b/user_guide_src/source/testing/response/032.php
@@ -0,0 +1,11 @@
+ ['key-a', 'key-b'],
+ * ]
+ */
+
+// Is true
+$result->assertJSONFragment(['config' => ['key-a']]);
diff --git a/user_guide_src/source/tutorial/conclusion.rst b/user_guide_src/source/tutorial/conclusion.rst
index afb38465904b..94b8ba5e3f2d 100644
--- a/user_guide_src/source/tutorial/conclusion.rst
+++ b/user_guide_src/source/tutorial/conclusion.rst
@@ -10,8 +10,9 @@ design patterns, which you can expand upon.
Now that you've completed this tutorial, we recommend you check out the
rest of the documentation. CodeIgniter is often praised because of its
comprehensive documentation. Use this to your advantage and read the
-"Overview" and "General Topics" sections thoroughly. You should read
-the class and helper references when needed.
+:doc:`Overview ` and :doc:`/general/index`
+sections thoroughly. You should read
+the :doc:`Library ` and :doc:`/helpers/index` references when needed.
Every intermediate PHP programmer should be able to get the hang of
CodeIgniter within a few days.
@@ -19,4 +20,5 @@ CodeIgniter within a few days.
If you still have questions about the framework or your own CodeIgniter
code, you can:
-- Check out our `forums `_
+- Check out our `Forum `_
+- Check out our `Slack `_
diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst
index 604c2373314e..99d28abf3cdb 100644
--- a/user_guide_src/source/tutorial/create_news_items.rst
+++ b/user_guide_src/source/tutorial/create_news_items.rst
@@ -1,4 +1,4 @@
-Create news items
+Create News Items
#################
You now know how you can read data from a database using CodeIgniter, but
@@ -7,29 +7,29 @@ you'll expand your news controller and model created earlier to include
this functionality.
Enable CSRF Filter
-------------------
+******************
Before creating a form, let's enable the CSRF protection.
-Open the **app/Config/Filters.php** file and update the ``$methods`` property like the following::
+Open the **app/Config/Filters.php** file and update the ``$methods`` property like the following:
- public $methods = [
- 'post' => ['csrf'],
- ];
+.. literalinclude:: create_news_items/001.php
It configures the CSRF filter to be enabled for all **POST** requests.
You can read more about the CSRF protection in :doc:`Security ` library.
-Create a form
--------------
+.. Warning:: In general, if you use ``$methods`` filters, you should :ref:`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.
+
+Create a Form
+*************
To input data into the database, you need to create a form where you can
input the information to be stored. This means you'll be needing a form
with two fields, one for the title and one for the text. You'll derive
the slug from our title in the model. Create a new view at
-**app/Views/news/create.php**.
-
-::
+**app/Views/news/create.php**::
= esc($title) ?>
@@ -50,7 +50,7 @@ the slug from our title in the model. Create a new view at
There are probably only three things here that look unfamiliar.
-The ``= session()->getFlashdata('error') ?>`` function is used to report
+The ``session()->getFlashdata('error')`` function is used to report
errors related to CSRF protection.
The ``service('validation')->listErrors()`` function is used to report
@@ -60,32 +60,10 @@ The ``csrf_field()`` function creates a hidden input with a CSRF token that help
Go back to your ``News`` controller. You're going to do two things here,
check whether the form was submitted and whether the submitted data
-passed the validation rules. You'll use the :doc:`form
-validation <../libraries/validation>` library to do this.
-
-::
-
- public function create()
- {
- $model = model(NewsModel::class);
-
- if ($this->request->getMethod() === 'post' && $this->validate([
- 'title' => 'required|min_length[3]|max_length[255]',
- 'body' => 'required',
- ])) {
- $model->save([
- 'title' => $this->request->getPost('title'),
- 'slug' => url_title($this->request->getPost('title'), '-', true),
- 'body' => $this->request->getPost('body'),
- ]);
-
- echo view('news/success');
- } else {
- echo view('templates/header', ['title' => 'Create a news item']);
- echo view('news/create');
- echo view('templates/footer');
- }
- }
+passed the validation rules.
+You'll use the :ref:`validation method in Controller ` to do this.
+
+.. literalinclude:: create_news_items/002.php
The code above adds a lot of functionality. First we load the NewsModel.
After that, we check if we deal with the **POST** request and then
@@ -109,14 +87,12 @@ slug, perfect for creating URIs.
After this, a view is loaded to display a success message. Create a view at
**app/Views/news/success.php** and write a success message.
-This could be as simple as:
-
-::
+This could be as simple as::
News item created successfully.
Model Updating
--------------------------------------------------------
+**************
The only thing that remains is ensuring that your model is set up
to allow data to be saved properly. The ``save()`` method that was
@@ -130,20 +106,7 @@ not actually save any data because it doesn't know what fields are
safe to be updated. Edit the **NewsModel** to provide it a list of updatable
fields in the ``$allowedFields`` property.
-::
-
- `.
-::
-
- $routes->match(['get', 'post'], 'news/create', 'News::create');
- $routes->get('news/(:segment)', 'News::view/$1');
- $routes->get('news', 'News::index');
- $routes->get('(:any)', 'Pages::view/$1');
+.. literalinclude:: create_news_items/004.php
Now point your browser to your local development environment where you
installed CodeIgniter and add ``/news/create`` to the URL.
@@ -182,7 +140,7 @@ Add some news and check out the different pages you made.
:width: 45%
Congratulations
--------------------------------------------------------
+***************
You just completed your first CodeIgniter4 application!
diff --git a/user_guide_src/source/tutorial/create_news_items/001.php b/user_guide_src/source/tutorial/create_news_items/001.php
new file mode 100644
index 000000000000..4e6615b6bbda
--- /dev/null
+++ b/user_guide_src/source/tutorial/create_news_items/001.php
@@ -0,0 +1,14 @@
+ ['csrf'],
+ ];
+
+ // ...
+}
diff --git a/user_guide_src/source/tutorial/create_news_items/002.php b/user_guide_src/source/tutorial/create_news_items/002.php
new file mode 100644
index 000000000000..66ff5e26776b
--- /dev/null
+++ b/user_guide_src/source/tutorial/create_news_items/002.php
@@ -0,0 +1,28 @@
+request->getMethod() === 'post' && $this->validate([
+ 'title' => 'required|min_length[3]|max_length[255]',
+ 'body' => 'required',
+ ])) {
+ $model->save([
+ 'title' => $this->request->getPost('title'),
+ 'slug' => url_title($this->request->getPost('title'), '-', true),
+ 'body' => $this->request->getPost('body'),
+ ]);
+
+ return view('news/success');
+ }
+
+ return view('templates/header', ['title' => 'Create a news item'])
+ . view('news/create')
+ . view('templates/footer');
+ }
+}
diff --git a/user_guide_src/source/tutorial/create_news_items/003.php b/user_guide_src/source/tutorial/create_news_items/003.php
new file mode 100644
index 000000000000..83e03f18eb2b
--- /dev/null
+++ b/user_guide_src/source/tutorial/create_news_items/003.php
@@ -0,0 +1,12 @@
+match(['get', 'post'], 'news/create', 'News::create');
+$routes->get('news/(:segment)', 'News::view/$1');
+$routes->get('news', 'News::index');
+$routes->get('pages', 'Pages::index');
+$routes->get('(:any)', 'Pages::view/$1');
+
+// ...
diff --git a/user_guide_src/source/tutorial/index.rst b/user_guide_src/source/tutorial/index.rst
index 798341a6ce96..e940ec849057 100644
--- a/user_guide_src/source/tutorial/index.rst
+++ b/user_guide_src/source/tutorial/index.rst
@@ -22,7 +22,7 @@ This tutorial will primarily focus on:
- Model-View-Controller basics
- Routing basics
- Form validation
-- Performing basic database queries using CodeIgniter's "Query Builder"
+- Performing basic database queries using CodeIgniter's Model
The entire tutorial is split up over several pages, each explaining a
small part of the functionality of the CodeIgniter framework. You'll go
@@ -55,9 +55,7 @@ Getting Up and Running
You can download a release manually from the site, but for this tutorial we will
use the recommended way and install the AppStarter package through Composer.
-From your command line type the following:
-
-::
+From your command line type the following::
> composer create-project codeigniter4/appstarter ci-news
@@ -84,14 +82,11 @@ command line from the root of your project::
> php spark serve
-
The Welcome Page
****************
Now point your browser to the correct URL you will be greeted by a welcome screen.
-Try it now by heading to the following URL:
-
-::
+Try it now by heading to the following URL::
http://localhost:8080
@@ -127,6 +122,5 @@ There are a couple of things to note here:
Everything else should be clear when you see it.
-
Now that we know how to get started and how to debug a little, let's get started building this
small news application.
diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst
index 396c74255962..aca19a25cad3 100644
--- a/user_guide_src/source/tutorial/news_section.rst
+++ b/user_guide_src/source/tutorial/news_section.rst
@@ -1,13 +1,13 @@
-News section
-###############################################################################
+News Section
+############
In the last section, we went over some basic concepts of the framework
by writing a class that references static pages. We cleaned up the URI by
adding custom routing rules. Now it's time to introduce dynamic content
and start using a database.
-Create a database to work with
--------------------------------------------------------
+Create a Database to Work with
+******************************
The CodeIgniter installation assumes that you have set up an appropriate
database, as outlined in the :doc:`requirements `.
@@ -18,13 +18,7 @@ commands (mysql, MySQL Workbench, or phpMyAdmin).
You need to create a database that can be used for this tutorial,
and then configure CodeIgniter to use it.
-Using your database client, connect to your database and run the SQL command below (MySQL).
-Also, add some seed records. For now, we'll just show you the SQL statements needed
-to create the table, but you should be aware that this can be done programmatically
-once you are more familiar with CodeIgniter; you can read about :doc:`Migrations <../dbmgmt/migration>`
-and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later.
-
-::
+Using your database client, connect to your database and run the SQL command below (MySQL)::
CREATE TABLE news (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -35,27 +29,28 @@ and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later.
KEY slug (slug)
);
+Also, add some seed records. For now, we'll just show you the SQL statements needed
+to create the table, but you should be aware that this can be done programmatically
+once you are more familiar with CodeIgniter; you can read about :doc:`Migrations <../dbmgmt/migration>`
+and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later.
+
A note of interest: a "slug", in the context of web publishing, is a
user- and SEO-friendly short text used in a URL to identify and describe a resource.
-The seed records might be something like:
-
-::
+The seed records might be something like::
INSERT INTO news VALUES
(1,'Elvis sighted','elvis-sighted','Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app.'),
(2,'Say it isn\'t so!','say-it-isnt-so','Scientists conclude that some programmers have a sense of humor.'),
(3,'Caffeination, Yes!','caffeination-yes','World\'s largest coffee shop open onsite nested coffee shop for staff only.');
-Connect to your database
--------------------------------------------------------
+Connect to Your Database
+************************
The local configuration file, ``.env``, that you created when you installed
CodeIgniter, should have the database property settings uncommented and
set appropriately for the database you want to use. Make sure you've configured
-your database properly as described :doc:`here <../database/configuration>`.
-
-::
+your database properly as described :doc:`here <../database/configuration>`::
database.default.hostname = localhost
database.default.database = ci4tutorial
@@ -63,8 +58,8 @@ your database properly as described :doc:`here <../database/configuration>`.
database.default.password = root
database.default.DBDriver = MySQLi
-Setting up your model
--------------------------------------------------------
+Setting up Your Model
+*********************
Instead of writing database operations right in the controller, queries
should be placed in a model, so they can easily be reused later. Models
@@ -75,18 +70,7 @@ You can read more about it :doc:`here `.
Open up the **app/Models/** directory and create a new file called
**NewsModel.php** and add the following code.
-::
-
- ` — is used. This makes it
+abstraction layer that is included with CodeIgniter -
+:doc:`Query Builder <../database/query_builder>` - is used in the ``CodeIgnite\Model``. This makes it
possible to write your 'queries' once and make them work on :doc:`all
supported database systems <../intro/requirements>`. The Model class
also allows you to easily work with the Query Builder and provides
some additional tools to make working with data simpler. Add the
following code to your model.
-::
-
- public function getNews($slug = false)
- {
- if ($slug === false) {
- return $this->findAll();
- }
-
- return $this->where(['slug' => $slug])->first();
- }
+.. literalinclude:: news_section/002.php
With this code, you can perform two different queries. You can get all
news records, or get a news item by its slug. You might have
@@ -126,8 +101,8 @@ that use the Query Builder to run their commands on the current table, and
returning an array of results in the format of your choice. In this example,
``findAll()`` returns an array of array.
-Display the news
--------------------------------------------------------
+Display the News
+****************
Now that the queries are written, the model should be tied to the views
that are going to display the news items to the user. This could be done
@@ -135,30 +110,7 @@ in our ``Pages`` controller created earlier, but for the sake of clarity,
a new ``News`` controller is defined. Create the new controller at
**app/Controllers/News.php**.
-::
-
- getNews();
- }
-
- public function view($slug = null)
- {
- $model = model(NewsModel::class);
-
- $data['news'] = $model->getNews($slug);
- }
- }
+.. literalinclude:: news_section/003.php
Looking at the code, you may see some similarity with the files we
created earlier. First, it extends a core CodeIgniter class, ``Controller``,
@@ -179,21 +131,9 @@ news item to be returned.
Now the data is retrieved by the controller through our model, but
nothing is displayed yet. The next thing to do is, passing this data to
-the views. Modify the ``index()`` method to look like this::
-
- public function index()
- {
- $model = model(NewsModel::class);
+the views. Modify the ``index()`` method to look like this:
- $data = [
- 'news' => $model->getNews(),
- 'title' => 'News archive',
- ];
-
- echo view('templates/header', $data);
- echo view('news/overview', $data);
- echo view('templates/footer', $data);
- }
+.. literalinclude:: news_section/004.php
The code above gets all news records from the model and assigns it to a
variable. The value for the title is also assigned to the ``$data['title']``
@@ -201,31 +141,7 @@ element and all data is passed to the views. You now need to create a
view to render the news items. Create **app/Views/news/overview.php**
and add the next piece of code.
-::
-
-
-
-
-
+.. literalinclude:: news_section/005.php
.. note:: We are again using using ``esc()`` to help prevent XSS attacks.
But this time we also passed "url" as a second parameter. That's because
@@ -243,50 +159,25 @@ a way that it can easily be used for this functionality. You only need to
add some code to the controller and create a new view. Go back to the
``News`` controller and update the ``view()`` method with the following:
-::
-
- public function view($slug = null)
- {
- $model = model(NewsModel::class);
-
- $data['news'] = $model->getNews($slug);
-
- if (empty($data['news'])) {
- throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the news item: ' . $slug);
- }
-
- $data['title'] = $data['news']['title'];
-
- echo view('templates/header', $data);
- echo view('news/view', $data);
- echo view('templates/footer', $data);
- }
+.. literalinclude:: news_section/006.php
Instead of calling the ``getNews()`` method without a parameter, the
``$slug`` variable is passed, so it will return the specific news item.
The only thing left to do is create the corresponding view at
**app/Views/news/view.php**. Put the following code in this file.
-::
-
-
= esc($news['title']) ?>
-
= esc($news['body']) ?>
+.. literalinclude:: news_section/007.php
Routing
--------------------------------------------------------
+*******
-Because of the wildcard routing rule created earlier, you need an extra
-route to view the controller that you just made. Modify your routing file
+Modify your routing file
(**app/Config/Routes.php**) so it looks as follows.
This makes sure the requests reach the ``News`` controller instead of
going directly to the ``Pages`` controller. The first line routes URI's
with a slug to the ``view()`` method in the ``News`` controller.
-::
-
- $routes->get('news/(:segment)', 'News::view/$1');
- $routes->get('news', 'News::index');
- $routes->get('(:any)', 'Pages::view/$1');
+.. literalinclude:: news_section/008.php
Point your browser to your "news" page, i.e., ``localhost:8080/news``,
you should see a list of the news items, each of which has a link
diff --git a/user_guide_src/source/tutorial/news_section/001.php b/user_guide_src/source/tutorial/news_section/001.php
new file mode 100644
index 000000000000..b565d4a82f25
--- /dev/null
+++ b/user_guide_src/source/tutorial/news_section/001.php
@@ -0,0 +1,10 @@
+findAll();
+ }
+
+ return $this->where(['slug' => $slug])->first();
+ }
+}
diff --git a/user_guide_src/source/tutorial/news_section/003.php b/user_guide_src/source/tutorial/news_section/003.php
new file mode 100644
index 000000000000..13c3b7263db6
--- /dev/null
+++ b/user_guide_src/source/tutorial/news_section/003.php
@@ -0,0 +1,22 @@
+getNews();
+ }
+
+ public function view($slug = null)
+ {
+ $model = model(NewsModel::class);
+
+ $data['news'] = $model->getNews($slug);
+ }
+}
diff --git a/user_guide_src/source/tutorial/news_section/004.php b/user_guide_src/source/tutorial/news_section/004.php
new file mode 100644
index 000000000000..9e823b6f7589
--- /dev/null
+++ b/user_guide_src/source/tutorial/news_section/004.php
@@ -0,0 +1,22 @@
+ $model->getNews(),
+ 'title' => 'News archive',
+ ];
+
+ return view('templates/header', $data)
+ . view('news/overview')
+ . view('templates/footer');
+ }
+}
diff --git a/user_guide_src/source/tutorial/news_section/005.php b/user_guide_src/source/tutorial/news_section/005.php
new file mode 100644
index 000000000000..39db0a4319f7
--- /dev/null
+++ b/user_guide_src/source/tutorial/news_section/005.php
@@ -0,0 +1,22 @@
+