diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c0eabf575 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[composer.json] +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..3c6095d2a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,38 @@ +const rules = require('@silverstripe/eslint-config/.eslintrc'); + +rules.plugins = ['markdown']; +rules.overrides = [ + { + files: ['**/*.md'], + processor: 'markdown/markdown' + }, + { + files: ['**/*.md/*.js'], + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + }, + settings: { + react: { + version: '16' + } + }, + rules: { + // These rules are not appropriate for linting markdown code blocks + 'lines-around-comment': 'off', + 'import/no-unresolved': 'off', + 'import/extensions': 'off', + 'react/jsx-no-undef': 'off', + 'no-undef': 'off', + 'no-unused-expressions': 'off', + 'no-unused-vars': 'off', + 'brace-style': 'off', // it's useful to have comments before the else block + // These rules are disabled because they are difficult to adhere to right now + 'jsx-a11y/label-has-associated-control': 'off', + 'react/prefer-stateless-function': 'off', + } + } +]; + +module.exports = rules; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d74b9db89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + # set fail-fast to false prevent one matrix job from cancelling other matrix jobs + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast + fail-fast: false + matrix: + script: [ 'lint-md', 'lint-js', 'lint-php' ] + name: ${{ matrix.script }} + steps: + + - name: Checkout code + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + + - name: Read .nvmrc + id: read-nvm + run: | + NPM_VERSION=$(cat .nvmrc) + echo "version=$NPM_VERSION" >> $GITHUB_OUTPUT + + - name: Install NPM + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 + with: + node-version: ${{ steps.read-nvm.outputs.version }} + + - name: Install yarn dependencies + run: | + npm install --global yarn + yarn install + + - name: Install PHP + if: ${{ matrix.script == 'lint-php' }} + uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2.22.0 + with: + php-version: 8.1 + + - name: Install composer dependencies + if: ${{ matrix.script == 'lint-php' }} + run: composer install --prefer-dist --no-progress --ansi --no-interaction --optimize-autoloader + + - name: Run lint + run: yarn ${{ matrix.script }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..afccd24b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/vendor/ +composer.lock diff --git a/.markdownlint-cli2.mjs b/.markdownlint-cli2.mjs new file mode 100644 index 000000000..236af090b --- /dev/null +++ b/.markdownlint-cli2.mjs @@ -0,0 +1,13 @@ + +import markdownlint from 'markdownlint'; +import enhancedProperNames from 'markdownlint-rule-enhanced-proper-names/src/enhanced-proper-names.js'; +import titleCaseStyle from 'markdownlint-rule-title-case-style'; +import { load } from 'js-yaml'; + +export default { + 'customRules': [ + enhancedProperNames, + titleCaseStyle, + ], + 'config': markdownlint.readConfigSync('./.markdownlint.yml', [ load ]), +}; diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 000000000..0978879a2 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,155 @@ +# Enable all rules with default settings as a baseline +default: true + +# MD041: Ignore the frontmatter (metadata) title when checking for H1s +first-line-h1: + front_matter_title: '' + +# MD025: Ignore the frontmatter (metadata) title when checking for H1s +single-h1: + front_matter_title: '' + +# MD003: Enforce ATX style headings +heading-style: + style: 'atx' + +# MD049: Use asterisks for italics +emphasis-style: + style: 'asterisk' + +# MD050: Use asterisks for bold +strong-style: + style: 'asterisk' + +# MD004: Use hyphens for unordered lists +ul-style: + style: 'dash' + +# MD029: Always use 1. for ordered lists +ol-prefix: + style: 'one' + +# MD013: Disable line-length rule for now as it touches too many lines of doc +line-length: false +# line_length: 120 + +# MD010: Use two spaces for each tab (default was 1) +no-hard-tabs: + spaces_per_tab: 2 + +# MD031: Don't require empty lines after code blocks in lists +blanks-around-fences: + list_items: false + +# MD035: Enforce a style for horizontal rules. +# Hyphens would be confusing since we use those for frontmatter (metadata) +hr-style: + style: '___' + +# MD046: Don't allow indented codeblocks +code-block-style: + style: 'fenced' + +# MD048: Use backticks for codeblocks +code-fence-style: + style: 'backtick' + +# MD040: Explicitly only allow some languages for code blocks +# This helps with consistency (e.g. avoid having both yml and yaml) +fenced-code-language: + language_only: true + allowed_languages: + - 'bash' # use this instead of shell or env + - 'css' + - 'diff' + - 'graphql' + - 'html' + - 'js' + - 'json' + - 'php' + - 'scss' + - 'ss' + - 'sql' + - 'text' + - 'xml' + - 'yml' + +# MD044: Disable in favour of the enhanced version which ignores custom anchors for headings +# markdownlint-rule-enhanced-proper-names: Enforces capitalisation for specific names +proper-names: off +enhanced-proper-names: + code_blocks: false + heading_id: false + names: + - 'API' + - 'type/api-break' # the GitHub label + - 'CI' + - 'CMS' + - '/cms' # e.g. "silverstripe/cms" + - '-cms' # e.g. "silverstripe/recipe-cms" + - 'CSS' + - 'GitHub' + - 'GraphQL' + - '/graphql' # e.g. "silverstripe/graphql" + - 'HTTP' + - 'JavaScript' + - 'JS' + - '.js' # e.g. "Node.js" + - 'jQuery' + - 'ORM' + - 'PHP' + - 'php-' # e.g. "php-intl extension" + - 'SCSS' + - 'Silverstripe' + - 'silverstripe/' # e.g. "silverstripe/framework" + - 'silverstripe-' # e.g. "silverstripe-vendormodule" + - '@silverstripe.org' + - 'TinyMCE' + - 'UI' + - 'URL' + - 'YAML' + +# markdownlint-rule-title-case-style: Use sentence-style headings +title-case-style: + case: 'sentence' + # commas in the following list are intentional and necessary since the plugin makes no distinction + # between words and punctuation + ignore: + - 'Apache' + - 'APIs' + - 'Composer' + - 'CTE' + - 'GitHub' + - 'GraphQL' + - 'Huntr' + - 'JavaScript' + - 'I' + - 'InnoDB' + - 'Git' + - 'jQuery' + - 'jQuery,' + - 'Lighttpd' + - 'MyISAM' + - 'MySQL' + - 'Nginx' + - 'Nginx,' + - 'PHPUnit' + - 'RFCs' + - 'Silverstripe' + - 'TinyMCE' + - 'Transifex' + - 'URLs' + - 'WebP' + +# MD033: Allow specific HTML tags +no-inline-html: + allowed_elements: + # br is necessary for new lines in tables + - 'br' + # accordians are okay + - 'details' + - 'summary' + # description lists are okay + - 'dl' + - 'dd' + - 'dt' diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..3c032078a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/composer.json b/composer.json index ad5798a53..cf333db3b 100644 --- a/composer.json +++ b/composer.json @@ -19,5 +19,14 @@ "name": "The SilverStripe Community", "homepage": "https://silverstripe.org" } - ] + ], + "require-dev": { + "silverstripe/markdown-php-codesniffer": "^1", + "slevomat/coding-standard": "^8.14" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/en/00_Getting_Started/00_Server_Requirements.md b/en/00_Getting_Started/00_Server_Requirements.md index 87776bb72..9d32444e6 100644 --- a/en/00_Getting_Started/00_Server_Requirements.md +++ b/en/00_Getting_Started/00_Server_Requirements.md @@ -12,11 +12,11 @@ the server to update templates, website logic, and perform upgrades or maintenan ## PHP -* PHP >=8.1, <=8.2 -* PHP extensions: `ctype`, `dom`, `fileinfo`, `hash`, `intl`, `mbstring`, `session`, `simplexml`, `tokenizer`, `xml` -* PHP configuration: `memory_limit` with at least `48M` -* PHP extension for image manipulation: Either `gd` or `imagick` -* PHP extension for a database connector (e.g. `mysql`) +- PHP >=8.1, <=8.2 +- PHP extensions: `ctype`, `dom`, `fileinfo`, `hash`, `intl`, `mbstring`, `session`, `simplexml`, `tokenizer`, `xml` +- PHP configuration: `memory_limit` with at least `48M` +- PHP extension for image manipulation: Either `gd` or `imagick` +- PHP extension for a database connector (e.g. `mysql`) Use [phpinfo()](https://php.net/manual/en/function.phpinfo.php) to inspect your configuration. @@ -26,13 +26,13 @@ You also need to install [Composer 2](https://getcomposer.org/). ## Database -* MySQL >=5.6 (built-in, [commercially supported](/project_governance/supported_modules/)) -* PostgreSQL ([third party module](https://github.com/silverstripe/silverstripe-postgresql), community +- MySQL >=5.6 (built-in, [commercially supported](/project_governance/supported_modules/)) +- PostgreSQL ([third party module](https://github.com/silverstripe/silverstripe-postgresql), community supported) -* SQL Server ([third party module](https://github.com/silverstripe/silverstripe-mssql), community supported) -* SQLite ([third party module](https://github.com/silverstripe/silverstripe-sqlite3), community supported) +- SQL Server ([third party module](https://github.com/silverstripe/silverstripe-mssql), community supported) +- SQLite ([third party module](https://github.com/silverstripe/silverstripe-sqlite3), community supported) -### Default MySQL Collation +### Default MySQL collation New projects default to the `utf8mb4_unicode_ci` collation when running against MySQL, which offers better support for multi-byte characters such as emoji. However, this may cause issues @@ -53,7 +53,7 @@ setting. It is generally recommended to leave this setting as-is because it resu some advanced cases, the sql_mode can be configured on the database connection via the configuration API ( see `MySQLDatabase::$sql_mode` for more details.) -### MySQL/MariaDB Int width in schema +### MySQL/MariaDB int width in schema MySQL 8.0.17 stopped reporting the width attribute for integers while MariaDB did not change its behaviour. This results in constant rebuilding of the schema when MySQLSchemaManager expects a field to look like e.g. @@ -65,7 +65,7 @@ SilverStripe\ORM\Connect\MySQLSchemaManager: schema_use_int_width: true # or false when INT widths should be ignored ``` -## Webserver Configuration +## Webserver configuration ### Overview @@ -83,16 +83,16 @@ be considered publicly accessible unless there are explicit webserver rules to p During runtime, Silverstripe CMS needs read access for the webserver user to your webroot. It also needs write access for the webserver user to the following locations: -* `public/assets/`: Used by the CMS and other logic to [store uploads](/developer_guides/files/file_storage) -* `TEMP_PATH`: Temporary file storage used for the default filesystem-based cache adapters in +- `public/assets/`: Used by the CMS and other logic to [store uploads](/developer_guides/files/file_storage) +- `TEMP_PATH`: Temporary file storage used for the default filesystem-based cache adapters in [Manifests](/developer_guides/execution_pipeline/manifests), [Object Caching](/developer_guides/performance/caching) and [Partial Template Caching](/developer_guides/templates/partial_template_caching). See [Environment Management](/getting_started/environment_management). -* `.graphql-generated`: silverstripe/graphql uses this directory. This is where your schema is +- `.graphql-generated`: silverstripe/graphql uses this directory. This is where your schema is stored once it [has been built](/developer_guides/graphql/getting_started/building_the_schema). Best practice - is to create it ahead of time, but if the directory doesn't exist and your project root is writable, the graphql + is to create it ahead of time, but if the directory doesn't exist and your project root is writable, the GraphQL module will create it for you. -* `public/_graphql`: silverstripe/graphql uses this directory. It's used for +- `public/_graphql`: silverstripe/graphql uses this directory. It's used for [schema introspection](/developer_guides/graphql/tips_and_tricks#schema-introspection). You should treat this folder the same way you treat the `.graphql-generated` folder. @@ -117,7 +117,7 @@ the `File.allowed_extensions` setting configured through file upload through Silverstripe CMS, so is considered a second line of defence. If you do not use apache to serve your website you should find out what equivalent configuration you need to set for your webserver. -### Secure Assets {#secure-assets} +### Secure assets {#secure-assets} Files can be kept in draft stage, and access restricted to certain user groups. These files are stored in a special `.protected/` folder (defaulting to `public/assets/.protected/`). @@ -142,7 +142,7 @@ SS_PROTECTED_ASSETS_PATH="../.protected/" The resulting folder structure will look as follows: -``` +```text .protected/ /my-protected-file.txt public/ @@ -155,7 +155,7 @@ app/ Don't forget to include this additional folder in any syncing and backup processes! -### Building, Packaging and Deployment {#building-packaging-deployment} +### Building, packaging and deployment {#building-packaging-deployment} It is common to build a Silverstripe CMS application into a package on one environment (e.g. a CI server), and then deploy the package to a (separate) webserver environment(s). This approach relies on all auto-generated files required by @@ -164,19 +164,19 @@ Silverstripe CMS to be included in the package, or generated on the fly on each The easiest way to ensure this is to commit auto generated files to source control. If those changes are considered too noisy, here's some pointers for auto-generated files to trigger and include in a deployment package: -* `public/_resources/`: Frontend resources copied from the (inaccessible) `vendor/` folder +- `public/_resources/`: Frontend resources copied from the (inaccessible) `vendor/` folder via [silverstripe/vendor-plugin](https://github.com/silverstripe/vendor-plugin). See [Templates: Requirements](/developer_guides/templates/requirements#exposing-resources-webroot). -* `.graphql-generated/` and `public/_graphql/`: Schema and type definitions required by CMS and any GraphQL API endpoint. +- `.graphql-generated/` and `public/_graphql/`: Schema and type definitions required by CMS and any GraphQL API endpoint. Generated by [silverstripe/graphql](https://github.com/silverstripe/silverstripe-graphql). See [building the schema](/developer_guides/graphql/getting_started/building_the_schema) and [deploying the schema](/developer_guides/graphql/getting_started/deploying_the_schema). -* Various recipes create default files in `app/` and `public/` on `composer install` +- Various recipes create default files in `app/` and `public/` on `composer install` and `composer update` via [silverstripe/recipe-plugin](https://github.com/silverstripe/recipe-plugin). -### Web Worker Concurrency +### Web worker concurrency It's generally a good idea to run multiple workers to serve multiple HTTP requests to Silverstripe CMS concurrently. The exact number depends on your website needs. The CMS attempts to request multiple views concurrently. It also @@ -191,13 +191,13 @@ allows serving of files larger than your PHP memory limit. Please be aware that PHP's [max_execution_time](https://www.php.net/manual/en/function.set-time-limit.php), which can risk exhaustion of web worker pools for long-running downloads. -### URL Rewriting +### URL rewriting Silverstripe CMS expects URL paths to be rewritten to `public/index.php`. For Apache, this is preconfigured through `.htaccess` files, and requires using the `mod_rewrite` module. By default, the relevant configuration files are located in `public/.htaccess` and `public/assets/.htaccess`. -### HTTP Headers +### HTTP headers Silverstripe CMS can add HTTP headers to responses it handles directly. These headers are often sensitive, for example preventing HTTP caching for responses displaying data based on user sessions, or when serving protected assets. You need @@ -210,7 +210,7 @@ Silverstripe CMS relies on the `Host` header to construct URLs such as "reset pa the systems hosting it only allow valid values for this header. See [Developer Guide: Security - Request hostname forgery](/developer_guides/security/secure_coding#request-hostname-forgery). -### CDNs and other Reverse Proxies +### CDNs and other reverse proxies If your Silverstripe CMS site is hosted behind multiple HTTP layers, you're in charge of controlling which forwarded headers are considered valid, and which IPs can set them. @@ -259,23 +259,25 @@ Silverstripe CMS is known to work with Microsoft IIS, and generates `web.config` Additionally, there are community supported guides for installing Silverstripe CMS on various environments: -* [Hosting via Bitnami](https://bitnami.com/stack/silverstripe/virtual-machine): In the cloud or as a locally hosted +- [Hosting via Bitnami](https://bitnami.com/stack/silverstripe/virtual-machine): In the cloud or as a locally hosted virtual machine -* [Vagrant/Virtualbox with CentOS](https://forum.silverstripe.org/t/installing-via-vagrant-virtualbox-with-centos/2248) -* [macOS with Homebrew](https://forum.silverstripe.org/t/installing-on-osx-with-homebrew/2247) -* [macOS with MAMP](https://forum.silverstripe.org/t/installing-on-osx-with-mamp/2249) -* [Windows with WAMP](https://forum.silverstripe.org/t/installing-on-windows-via-wamp/2250) -* [Vagrant with silverstripe-australia/vagrant-environment](https://github.com/silverstripe-australia/vagrant-environment) -* [Vagrant with BetterBrief/vagrant-skeleton](https://github.com/BetterBrief/vagrant-skeleton) +- [Vagrant/Virtualbox with CentOS](https://forum.silverstripe.org/t/installing-via-vagrant-virtualbox-with-centos/2248) +- [macOS with Homebrew](https://forum.silverstripe.org/t/installing-on-osx-with-homebrew/2247) +- [macOS with MAMP](https://forum.silverstripe.org/t/installing-on-osx-with-mamp/2249) +- [Windows with WAMP](https://forum.silverstripe.org/t/installing-on-windows-via-wamp/2250) +- [Vagrant with silverstripe-australia/vagrant-environment](https://github.com/silverstripe-australia/vagrant-environment) +- [Vagrant with BetterBrief/vagrant-skeleton](https://github.com/BetterBrief/vagrant-skeleton) ### Email Silverstripe CMS uses [symfony/mailer](https://github.com/symfony/mailer) to send email messages. [silverstripe/framework](https://github.com/silverstripe/silverstripe-framework) is configured to use a `sendmail` binary (usually found in `/usr/sbin/sendmail`). Alternatively [email can be configured](/developer_guides/email/) to use SMTP or other mail transports instead of sendmail. -You _must_ ensure emails are being sent from your _production_ environment. You can do this by testing that the ***Lost password*** form available at `/Security/lostpassword` sends an email to your inbox, or with the following code snippet that can be run via a `SilverStripe\Dev\BuildTask`: +You *must* ensure emails are being sent from your *production* environment. You can do this by testing that the ***Lost password*** form available at `/Security/lostpassword` sends an email to your inbox, or with the following code snippet that can be run via a `SilverStripe\Dev\BuildTask`: ```php -$email = SilverStripe\Control\Email\Email::create('no-reply@mydomain.com', 'myuser@gmail.com', 'My test subject', 'My email body text'); +use SilverStripe\Control\Email\Email; + +$email = Email::create('no-reply@mydomain.com', 'myuser@gmail.com', 'My test subject', 'My email body text'); $email->send(); ``` @@ -283,7 +285,7 @@ Using the code snippet above also tests that the ability to set the "from" addre See the [email section](/developer_guides/email) for further details, including how to set the administrator "from" email address, change the `sendmail` binary location, and how to use SMTP or other mail transports instead of sendmail. -## PHP Requirements for older Silverstripe CMS releases {#php-support} +## PHP requirements for older Silverstripe CMS releases {#php-support} Silverstripe CMS's PHP support has changed over time and if you are looking to upgrade PHP on your Silverstripe CMS site, this table may be of use: diff --git a/en/00_Getting_Started/02_Composer.md b/en/00_Getting_Started/02_Composer.md index 98086a17c..ed7f425f2 100644 --- a/en/00_Getting_Started/02_Composer.md +++ b/en/00_Getting_Started/02_Composer.md @@ -108,7 +108,7 @@ Updates to the required modules will be installed, and the `composer.lock` file commits and version constraints for each of them. [hint] -The update command can also be used to _downgrade_ dependencies - if you edit your `composer.json` file and set a version +The update command can also be used to *downgrade* dependencies - if you edit your `composer.json` file and set a version constraint that will require a lower version to be installed, running `composer update` will "update" your installed dependencies to match your constraints, which in this case would install lower versions than what you had previously. @@ -127,22 +127,22 @@ version string. You can run `composer install` to install dependencies from this So your deployment process, as it relates to Composer, should be as follows: -* Run `composer update` on your development version before you start whatever testing you have planned. Perform all the +- Run `composer update` on your development version before you start whatever testing you have planned. Perform all the necessary testing. -* Check `composer.lock` into your repository. -* Deploy your project code base, using the deployment tool of your choice. -* Run `composer install --no-dev -o` on your production version. In this command, the `--no-dev` command tells Composer +- Check `composer.lock` into your repository. +- Deploy your project code base, using the deployment tool of your choice. +- Run `composer install --no-dev -o` on your production version. In this command, the `--no-dev` command tells Composer not to install your development-only dependencies, and `-o` is an alias for `--optimise-autoloader`, which will convert your PSR-0 and PSR-4 autoloader definitions into a classmap to improve the speed of the autoloader. -## Composer managed modules, Git and .gitignore +## Composer managed modules, Git and `.gitignore` Modules and themes managed by Composer should not be committed with your project's source code. Silverstripe CMS recipes ship with a [.gitignore](https://git-scm.com/docs/gitignore) file by default which prevents this. For more details read [Should I commit the dependencies in my vendor directory?](https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md) . -## Dev Environments for Contributing Code {#contributing} +## Dev environments for contributing code {#contributing} So you want to contribute to Silverstripe CMS? Fantastic! You can do this with Composer too. You have to tell Composer three things in order to be able to do this: @@ -166,9 +166,9 @@ The `--keep-vcs` flag will make sure you have access to the git history of the i It's also a good idea to require [`silverstripe/recipe-testing`](https://github.com/silverstripe/recipe-testing) as a dev dependency - it adds a few modules which are useful for Silverstripe CMS development: -* The `behat-extension` module allows running [Behat](https://behat.org) integration tests -* The `phpunit` library is used to run unit and functional tests -* The `php_codesniffer` library is used to lint PHP to ensure it adheres to our +- The `behat-extension` module allows running [Behat](https://behat.org) integration tests +- The `phpunit` library is used to run unit and functional tests +- The `php_codesniffer` library is used to lint PHP to ensure it adheres to our [coding conventions](/contributing/php_coding_conventions/#php-coding-conventions). Please read the [Contributing Code](/contributing/code) documentation to find out how to create forks and send pull @@ -176,7 +176,7 @@ requests. ## Advanced usage -### Manually editing composer.json +### Manually editing `composer.json` To remove dependencies, or if you prefer seeing your dependencies in a text file, you can edit the `composer.json` file. It will appear in your project root, and by default, it will look something like this: @@ -243,7 +243,7 @@ composer update ### Using development versions Composer will by default download the latest stable version of silverstripe/installer. The `composer.json` file that -comes with silverstripe/installer may also explicitly state it requires the stable version of cms and framework - this +comes with silverstripe/installer may also explicitly state it requires the stable version of CMS and framework - this is to ensure that when developers are getting started, running `composer update` won't upgrade their project to an unstable version @@ -268,19 +268,19 @@ composer create-project silverstripe/installer ./my-project 5.0.x-dev By default, Composer will install modules listed on the Packagist site. There are a few reasons that you might not want to do this. For example: -* You may have your own fork of a module, either specific to a project, or because you are working on a pull request -* You may have a module that hasn't been released to the public. +- You may have your own fork of a module, either specific to a project, or because you are working on a pull request +- You may have a module that hasn't been released to the public. There are many ways that you can address this, but this is one that we recommend, because it minimises the changes you would need to make to switch to an official version in the future. This is how you do it: -* **Ensure that all of your fork repositories have correct composer.json files.** Set up the project forks as you would - a distributed package. If you have cloned a repository that already has a composer.json file, then there's nothing you +- **Ensure that all of your fork repositories have correct `composer.json` files.** Set up the project forks as you would + a distributed package. If you have cloned a repository that already has a `composer.json` file, then there's nothing you need to do, but if not, you will need to create one yourself. -* **List all your fork repositories in your project's composer.json files.** You do this in a `repositories` section. +- **List all your fork repositories in your project's `composer.json` files.** You do this in a `repositories` section. Set the `type` to `vcs`, and `url` to the URL of the repository. The result will look something like this: ```json @@ -296,7 +296,7 @@ This is how you do it: } ``` -* **Install the module as you would normally.** Use the regular Composer commands - there are no special flags to use a +- **Install the module as you would normally.** Use the regular Composer commands - there are no special flags to use a fork. Your fork will be used in place of the package version, so long as it meets the dependency version constraint. ```bash @@ -371,7 +371,7 @@ the ["Aliases" chapter of the Composer documentation](https://getcomposer.org/do Follow the packagist.org advice on choosing a [unique name and vendor prefix](https://packagist.org/about#naming-your-package). Please don't use the `silverstripe/` vendor prefix, since that's reserved for modules produced by Silverstripe Ltd. In -order to declare that your module is in fact a Silverstripe CMS module, use the "silverstripe" tag in the composer.json +order to declare that your module is in fact a Silverstripe CMS module, use the `silverstripe` tag in the `composer.json` file, and set the "type" to `silverstripe-vendormodule`. ### What about themes? @@ -389,14 +389,14 @@ the live server hosts a git repository checkout, which is updated to push a newe run `composer install` checking out the code. We recommend looking into [Composer "lock" files](https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file) for this purpose. -### Can I keep using Downloads, Subversion Externals or Git Submodules? +### Can I keep using downloads, subversion externals or Git submodules? Composer is more than just a file downloader. It comes with additional features such as [autoloading](https://getcomposer.org/doc/01-basic-usage.md#autoloading) and [scripts](https://getcomposer.org/doc/articles/scripts.md) which some modules rely on. You really should be using Composer to manage your PHP dependencies. -### I don't want to get development versions of everything! +### I don't want to get development versions of everything You don't have to, Composer is designed to work on the constraints you set. You can declare the ["minimum-stability"](https://getcomposer.org/doc/04-schema.md#minimum-stability) diff --git a/en/00_Getting_Started/03_Environment_Management.md b/en/00_Getting_Started/03_Environment_Management.md index 26723076a..d9677051a 100644 --- a/en/00_Getting_Started/03_Environment_Management.md +++ b/en/00_Getting_Started/03_Environment_Management.md @@ -12,7 +12,7 @@ server. For each of these environments we may require slightly different configurations for our servers. This could be our debug level, caching backends, or - of course - sensitive information such as database credentials. -To manage environment variables, as well as other server globals, the [api:SilverStripe\Core\Environment] class provides +To manage environment variables, as well as other server globals, the [`Environment`](api:SilverStripe\Core\Environment) class provides a set of APIs and helpers. ## Security considerations @@ -24,7 +24,7 @@ environment variables. If you do use a `.env` file on your servers, you must ensure that external access to `.env` files is blocked by the webserver. -## Managing environment variables with .env files +## Managing environment variables with `.env` files By default a file named `.env` must be placed in your project root (ie: the same folder as your `composer.json`) or the parent directory. If this file exists, it will be automatically loaded by the framework and the environment variables @@ -74,9 +74,9 @@ Environment::setEnv('API_KEY', 'AABBCCDDEEFF012345'); ### Using environment variables in config To use environment variables in `.yaml` configs you can reference them using backticks. This only works in `Injector` configuration. -See the [Injector documentation](/developer_guides/extending/injector/#using-constants-and-environment-variables) for details. +See the [`Injector` documentation](/developer_guides/extending/injector/#using-constants-and-environment-variables) for details. -## Including an extra .env file +## Including an extra `.env` file Sometimes it may be useful to include an extra `.env` file - on a shared local development environment where all database credentials could be the same. To do this, you can add this snippet to your `app/_config.php` file: @@ -92,7 +92,7 @@ $loader->loadFile($env); ``` [warning] -Note that because `_config.php` is processed after yaml configuration, variables set in these extra `.env` files cannot be used inside yaml config. +Note that because `_config.php` is processed after YAML configuration, variables set in these extra `.env` files cannot be used inside YAML config. [/warning] ## Core environment variables @@ -121,7 +121,7 @@ Silverstripe core environment variables are listed here, though you're free to d | `SS_ENVIRONMENT_TYPE`| The environment type. Should be one of `dev`, `test`, or `live`. See [Environment Types](/developer_guides/debugging/environment_types/) for more information. | | `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges. | | `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database. | -| `SS_USE_BASIC_AUTH`| Baseline protection for requests handled by Silverstripe. Usually requires additional security measures for comprehensive protection. Set this to the name of a permission which users must have to be able to access the site (e.g. "ADMIN"). See [Environment Types](/developer_guides/debugging/environment_types) for caveats. | +| `SS_USE_BASIC_AUTH`| Baseline protection for requests handled by Silverstripe CMS. Usually requires additional security measures for comprehensive protection. Set this to the name of a permission which users must have to be able to access the site (e.g. "ADMIN"). See [Environment Types](/developer_guides/debugging/environment_types) for caveats. | | `SS_SEND_ALL_EMAILS_TO`| If you define this constant all emails will be redirected to this address, overriding the original address(es). | | `SS_SEND_ALL_EMAILS_FROM`| If you define this constant all emails will be sent from this address, overriding the original address. | | `SS_ERROR_LOG` | Path to a file for logging errors, relative to the project root. See [Logging and Error Handling](/developer_guides/debugging/error_handling/) for more information about error logging. | @@ -131,6 +131,6 @@ Silverstripe core environment variables are listed here, though you're free to d | `SS_ALLOWED_HOSTS` | A comma deliminated list of hostnames the site is allowed to respond to. See [Request hostname forgery](/developer_guides/security/secure_coding/#request-hostname-forgery) for more information. | | `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a `Psr\Cache\CacheItemPoolInterface`, `Psr\SimpleCache\CacheInterface`, or `SilverStripe\Core\Cache\CacheFactory` class implementation. | | `SS_IGNORE_DOT_ENV` | If set, the `.env` file will be ignored. This is good for live to mitigate any performance implications of loading the `.env` file. | -| `SS_BASE_URL` | The url to use when it isn't determinable by other means (eg: for CLI commands). Should either start with a protocol (e.g. `https://www.example.com`) or with a double forward slash (e.g. `//www.example.com`). | +| `SS_BASE_URL` | The URL to use when it isn't determinable by other means (for example for CLI commands). Should either start with a protocol (e.g. `https://www.example.com`) or with a double forward slash (e.g. `//www.example.com`). | | `SS_FLUSH_ON_DEPLOY` | Try to detect deployments through file system modifications and flush on the first request after every deploy. Does not run "dev/build" - only "flush". Possible values are `true` (check for a framework PHP file modification time), `false` (no checks, skip deploy detection) or a path to a specific file or folder to be checked. See [DeployFlushDiscoverer](api:SilverStripe\Core\Startup\DeployFlushDiscoverer) for more details.

False by default. | | `SS_TEMP_PATH` | File storage used for the default cache adapters in [Manifests](/developer_guides/execution_pipeline/manifests), [Object Caching](/developer_guides/performance/caching) and [Partial Template Caching](/developer_guides/templates/partial_template_caching). Can be an absolute path (with a leading `/`), or a path relative to the project root. Defaults to creating a sub-directory of PHP's built-in `sys_get_temp_dir()` or using the `silverstripe-cache` directory relative to the project root if one is present. | diff --git a/en/00_Getting_Started/04_Directory_Structure.md b/en/00_Getting_Started/04_Directory_Structure.md index 5f77f3b58..77aa71df8 100644 --- a/en/00_Getting_Started/04_Directory_Structure.md +++ b/en/00_Getting_Started/04_Directory_Structure.md @@ -4,14 +4,14 @@ summary: An overview of what each directory contains in a Silverstripe CMS insta icon: sitemap --- -# Directory Structure +# Directory structure ## Introduction The directory-structure in Silverstripe is built on "convention over configuration", so the placement of some files and directories is meaningful to its logic. -## Core Structure +## Core structure Directory | Description --------- | ----------- @@ -22,7 +22,7 @@ Directory | Description `vendor/` | Silverstripe modules and other supporting libraries (e.g. the framework is in `vendor/silverstripe/framework`) `themes/` | Standard theme installation location -## Custom Code Structure +## Custom code structure We use `app/` as the default folder. @@ -33,18 +33,17 @@ We use `app/` as the default folder. | `app/src` | PHP code specific to your application (subdirectories are optional) | | `app/tests` | PHP unit/functional/end-to-end tests | | `app/templates` | HTML [templates](/developer_guides/templates) with `*.ss-extension` for the `$default` theme | -| `app/client/src` | Conventional directory for source resources (images/css/javascript) for your CMS customisations | -| `app/client/dist` | Conventional directory for transpiled resources (images/css/javascript) for your CMS customisations | -| `app/client/lang` | Conventional directory for [javascript translation tables](/developer_guides/i18n/#translation-tables-in-javascript) | -| `app/lang` | Contains [yaml translation tables](/developer_guides/i18n/#language-definitions) | +| `app/client/src` | Conventional directory for source resources (images/CSS/JavaScript) for your CMS customisations | +| `app/client/dist` | Conventional directory for transpiled resources (images/CSS/JavaScript) for your CMS customisations | +| `app/client/lang` | Conventional directory for [JavaScript translation tables](/developer_guides/i18n/#translation-tables-in-javascript) | +| `app/lang` | Contains [YAML translation tables](/developer_guides/i18n/#language-definitions) | | `app/themes/` | Custom nested themes (note: theme structure is described below) | Arbitrary directory-names are allowed, as long as they don't collide with existing modules or the directories lists in "Core Structure". Here's how you would reconfigure your default folder to `myspecialapp`. -**`myspecialapp/_config/config.yml`** - ```yml +# myspecialapp/_config/config.yml --- Name: myspecialapp --- @@ -55,7 +54,7 @@ SilverStripe\Core\Manifest\ModuleManifest: Check our [JavaScript Coding Conventions](/contributing/javascript_coding_conventions/) for more details on folder and file naming in Silverstripe core modules. -## Themes Structure +## Themes structure | Directory | Description | | ------------------ | --------------------------- | @@ -66,7 +65,7 @@ Silverstripe core modules. See [themes](/developer_guides/templates/themes). -## Module Structure +## Module structure Modules are commonly stored as composer packages in the `vendor/` folder. They need to have a `_config.php` file or a `_config/` directory present, and should follow the same conventions as posed in "Custom Site Structure". @@ -112,7 +111,7 @@ include paths or `require()` calls in your own code - after adding a new class, a `flush=1` query parameter. See the ["Manifests" documentation](/developer_guides/execution_pipeline/manifests) for details. -## Best Practices +## Best practices ### Making /assets readonly diff --git a/en/00_Getting_Started/05_Recipes.md b/en/00_Getting_Started/05_Recipes.md index b986e26be..b673bb215 100644 --- a/en/00_Getting_Started/05_Recipes.md +++ b/en/00_Getting_Started/05_Recipes.md @@ -4,11 +4,11 @@ summary: What Recipes are, and how they are used in Silverstripe CMS icon: clipboard --- -# Adding features to your project with Recipes +# Adding features to your project with recipes To achieve more complex use cases in Silverstripe CMS, you may need to combine many modules and add extra configuration to integrate these together. Silverstripe CMS Recipes streamline this process for common use cases. -## What are Silverstripe CMS Recipes? +## What are Silverstripe CMS recipes? Recipes are used to implement common broad feature sets by shipping a collection of modules along with the relevant integration logic. They allow developers to quickly get started while retaining the ability to customise their integration to their specific needs. @@ -21,11 +21,12 @@ Silverstripe CMS is powered by a system of components in the form of Composer pa - **Modules**, which provide pieces of functionality (such as `silverstripe/cms` and `silverstripe/framework`) - **Recipes**, which group related Modules together to make them easier to install and release. -By design, modules tend to be small and serve a specific function. You may need to combine many modules to achieve a wider goal. +By design, modules tend to be small and serve a specific function. You may need to combine many modules to achieve a wider goal. For example, the [`silverstripe/blog`](https://github.com/silverstripe/silverstripe-blog) module by itself simply allows you to create blog posts. It does not include all the features you could want in a blog, like a comment system or widgets to display related content. The [`silverstripe/recipe-blog`](https://github.com/silverstripe/recipe-blog) recipe installs `silverstripe/blog` module, but also: + - [`silverstripe/widgets`](https://github.com/silverstripe/silverstripe-widgets) and [`silverstripe/content-widget`](https://github.com/silverstripe/silverstripe-content-widget) to display widgets - [`silverstripe/comments`](https://github.com/silverstripe/silverstripe-comments) and [`silverstripe/comment-notifications`](https://github.com/silverstripe/comment-notifications) to allow the management of comments on blog post - [`silverstripe/spamprotection`](https://github.com/silverstripe/silverstripe-spamprotection) to provide basic SPAM protection on comments. @@ -38,7 +39,7 @@ The Silverstripe CMS project maintains a number of recipes. Some third parties a ## Releasing supported recipes -When we announce a new release of Silverstripe CMS and publish a changelog for it, we refer to a new set of _recipe_ versions, which include new versions of some or all of their associated Modules. The easiest way to keep up to date with new Silverstripe CMS releases is to depend on one of the core Recipes: +When we announce a new release of Silverstripe CMS and publish a changelog for it, we refer to a new set of *recipe* versions, which include new versions of some or all of their associated modules. The easiest way to keep up to date with new Silverstripe CMS releases is to depend on one of the core recipes: - [`silverstripe/recipe-core`](https://packagist.org/packages/silverstripe/recipe-core): Contains only the base framework, without the admin UI or CMS features. diff --git a/en/00_Getting_Started/index.md b/en/00_Getting_Started/index.md index 7f3853a1c..c3fa616ef 100644 --- a/en/00_Getting_Started/index.md +++ b/en/00_Getting_Started/index.md @@ -4,7 +4,9 @@ introduction: Silverstripe is a web application. This means that you will need t icon: rocket --- -## Server Requirements +# Getting started + +## Server requirements Silverstripe requires PHP 8.1 or newer. It runs on many webservers and databases, but is most commonly served using Apache and MySQL/MariaDB. @@ -12,7 +14,7 @@ Apache and MySQL/MariaDB. If you are setting up your own environment, you'll need to consider a few configuration settings such as URL rewriting and protecting access to certain files. Refer to our [server requirements](server_requirements) for details. -## Quickstart Installation +## Quickstart installation If you're running Apache with MySQL/MariaDB already, and know your way around webservers, follow these steps to get started. Silverstripe is installed via [Composer](https://getcomposer.org), a package management tool for PHP that lets @@ -55,7 +57,7 @@ Your website should be available on your domain now (e.g. `https://www.example.c For more information on how to maintain your installation or install projects, check out [Using Silverstripe with Composer](composer). -## Guided Installation +## Guided installation If you are unsure on how this all works, please jump on our [lessons](https://www.silverstripe.org/learn/lessons/v4/). Webserver setup is covered in diff --git a/en/01_Lessons/index.md b/en/01_Lessons/index.md index de791a04e..39fefd5b1 100644 --- a/en/01_Lessons/index.md +++ b/en/01_Lessons/index.md @@ -4,31 +4,31 @@ introduction: The lessons take a step by step look at how to build a Silverstrip icon: graduation-cap --- -* [How to set up a local development environment in Silverstripe CMS](https://www.silverstripe.org/learn/lessons/v4/up-and-running-setting-up-a-local-silverstripe-dev-environment-1) -* [Lesson 1: Creating your first project](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-project) -* [Lesson 2: Migrating static templates into your theme](https://www.silverstripe.org/learn/lessons/v4/migrating-static-templates-into-your-theme-1) -* [Lesson 3: Adding dynamic content](https://www.silverstripe.org/learn/lessons/v4/adding-dynamic-content-1) -* [Lesson 4: Working with multiple templates](https://www.silverstripe.org/learn/lessons/v4/working-with-multiple-templates-1) -* [Lesson 5: The holder/page pattern](https://www.silverstripe.org/learn/lessons/v4/the-holderpage-pattern-1) -* [Lesson 6: Adding Custom Fields to a Page](https://www.silverstripe.org/learn/lessons/v4/adding-custom-fields-to-a-page-1) -* [Lesson 7: Working with Files and Images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) -* [Lesson 8: Introduction to the ORM](https://www.silverstripe.org/learn/lessons/v4/introduction-to-the-orm-1) -* [Lesson 9: Data Relationships - $has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) -* [Lesson 10: Data Relationships - $many_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1) -* [Lesson 11: Introduction to frontend forms](https://www.silverstripe.org/learn/lessons/v4/introduction-to-frontend-forms-1) -* [Lesson 12: Data Extensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1) -* [Lesson 13: Introduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) -* [Lesson 14: Controller Actions/DataObjects as Pages](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1) -* [Lesson 15: Building a Search Form](https://www.silverstripe.org/learn/lessons/v4/building-a-search-form-1) -* [Lesson 16: Lists and Pagination](https://www.silverstripe.org/learn/lessons/v4/lists-and-pagination-1) -* [Lesson 17: Ajax Behaviour and Viewable Data](https://www.silverstripe.org/learn/lessons/v4/ajax-behaviour-and-viewabledata-1) -* [Lesson 18: Dealing with Arbitrary Template Data](https://www.silverstripe.org/learn/lessons/v4/dealing-with-arbitrary-template-data-1) -* [Lesson 19: Creating Filtered Views](https://www.silverstripe.org/learn/lessons/v4/creating-filtered-views-1) -* [Lesson 20: Beyond the ORM: Building Custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1) -* [Lesson 21: Advanced Environment Configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) +# Lessons -## Help: If you get stuck +- [How to set up a local development environment in Silverstripe CMS](https://www.silverstripe.org/learn/lessons/v4/up-and-running-setting-up-a-local-silverstripe-dev-environment-1) +- [Lesson 1: Creating your first project](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-project) +- [Lesson 2: Migrating static templates into your theme](https://www.silverstripe.org/learn/lessons/v4/migrating-static-templates-into-your-theme-1) +- [Lesson 3: Adding dynamic content](https://www.silverstripe.org/learn/lessons/v4/adding-dynamic-content-1) +- [Lesson 4: Working with multiple templates](https://www.silverstripe.org/learn/lessons/v4/working-with-multiple-templates-1) +- [Lesson 5: The holder/page pattern](https://www.silverstripe.org/learn/lessons/v4/the-holderpage-pattern-1) +- [Lesson 6: Adding Custom Fields to a Page](https://www.silverstripe.org/learn/lessons/v4/adding-custom-fields-to-a-page-1) +- [Lesson 7: Working with Files and Images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) +- [Lesson 8: Introduction to the ORM](https://www.silverstripe.org/learn/lessons/v4/introduction-to-the-orm-1) +- [Lesson 9: Data Relationships - $has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) +- [Lesson 10: Data Relationships - $many_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1) +- [Lesson 11: Introduction to frontend forms](https://www.silverstripe.org/learn/lessons/v4/introduction-to-frontend-forms-1) +- [Lesson 12: Data Extensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1) +- [Lesson 13: Introduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) +- [Lesson 14: Controller Actions/DataObjects as Pages](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1) +- [Lesson 15: Building a Search Form](https://www.silverstripe.org/learn/lessons/v4/building-a-search-form-1) +- [Lesson 16: Lists and Pagination](https://www.silverstripe.org/learn/lessons/v4/lists-and-pagination-1) +- [Lesson 17: Ajax Behaviour and Viewable Data](https://www.silverstripe.org/learn/lessons/v4/ajax-behaviour-and-viewabledata-1) +- [Lesson 18: Dealing with Arbitrary Template Data](https://www.silverstripe.org/learn/lessons/v4/dealing-with-arbitrary-template-data-1) +- [Lesson 19: Creating Filtered Views](https://www.silverstripe.org/learn/lessons/v4/creating-filtered-views-1) +- [Lesson 20: Beyond the ORM: Building Custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1) +- [Lesson 21: Advanced Environment Configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) -* [The Tips & Tricks forum](https://forum.silverstripe.org/c/tips): Review some existing solutions to common problems. -* [Silverstripe CMS Community](https://www.silverstripe.org/community/): Join our community chat via Slack, or ask a question +- [The Tips & Tricks forum](https://forum.silverstripe.org/c/tips): Review some existing solutions to common problems. +- [Silverstripe CMS Community](https://www.silverstripe.org/community/): Join our community chat via Slack, or ask a question on Stack Overflow. diff --git a/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md b/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md index b9e51b6d4..44657aaaf 100644 --- a/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md +++ b/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md @@ -4,14 +4,14 @@ summary: Introduction to creating and querying a database records through the OR icon: database --- -# Introduction to the Data Model and ORM +# Introduction to the data model and ORM Silverstripe uses an [object-relational mapping](https://en.wikipedia.org/wiki/Object-relational_mapping) (aka "ORM") to represent its information. -* Each database table maps to a PHP class. -* Each database row maps to a PHP object. -* Each database column maps to a property on a PHP object. +- Each database table maps to a PHP class. +- Each database row maps to a PHP object. +- Each database column maps to a property on a PHP object. All data tables in Silverstripe CMS are defined as subclasses of [DataObject](api:SilverStripe\ORM\DataObject). The [DataObject](api:SilverStripe\ORM\DataObject) class represents a single row in a database table, following the ["Active Record"](https://en.wikipedia.org/wiki/Active_record_pattern) @@ -20,12 +20,13 @@ along with any [relationships](relations) defined as `$has_one`, `$has_many`, `$ Let's look at a simple example: -**app/src/Player.php** - ```php +// app/src/Model/Player.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $table_name = 'Player'; @@ -43,12 +44,12 @@ This `Player` class definition will create a database table `Player` with column so on. After writing this class, we need to regenerate the database schema. [hint] -You can technically omit the `table_name` property, and a default table name will be created based on the fully qualified class name - but this can result in table names that are too long for the database engine to handle. We recommend that you _always_ explicitly declare a table name for your models. +You can technically omit the `table_name` property, and a default table name will be created based on the fully qualified class name - but this can result in table names that are too long for the database engine to handle. We recommend that you *always* explicitly declare a table name for your models. See more in [Mapping classes to tables with `DataObjectSchema`](#mapping-classes-to-tables) below. [/hint] -## Generating the Database Schema +## Generating the database schema After adding, modifying or removing `DataObject` subclasses, make sure to rebuild your Silverstripe CMS database. The database schema is generated automatically by visiting `/dev/build` (e.g. `https://www.example.com/dev/build`) in your browser @@ -63,27 +64,27 @@ as required. It will perform the following changes: -* Create any missing tables -* Create any missing field columns -* Create any missing indexes -* Alter the field type of any existing fields -* Rename any obsolete tables that it previously created to `_obsolete_tablename` (e.g. `_obsolete_player`) - * Obsolete tables are only renamed if the `DataObject` model which owns the table has no need for the table (e.g. no fields are declared for the model, and it is a subclass of some other `DataObject`). +- Create any missing tables +- Create any missing field columns +- Create any missing indexes +- Alter the field type of any existing fields +- Rename any obsolete tables that it previously created to `_obsolete_tablename` (e.g. `_obsolete_player`) + - Obsolete tables are only renamed if the `DataObject` model which owns the table has no need for the table (e.g. no fields are declared for the model, and it is a subclass of some other `DataObject`). It **won't** do any of the following -* Delete tables -* Delete field columns -* Rename any tables that it doesn't recognize. This allows other applications to coexist in the same database, as long as +- Delete tables +- Delete field columns +- Rename any tables that it doesn't recognize. This allows other applications to coexist in the same database, as long as their table names don't match a Silverstripe CMS data class. When rebuilding the database schema through the [ClassLoader](api:SilverStripe\Core\Manifest\ClassLoader) the following additional fields are automatically set on the `DataObject`. -* `ID`: Primary Key. This will use the database's built-in auto-numbering system on the base table, and apply the same ID to all subclass tables. -* `ClassName`: An enumeration listing this data-class and all of its subclasses. The value is the actual `DataObject` subclass used to write the record. -* `Created`: A date/time field set to the creation date (i.e. when it was first written to the database) of this record -* `LastEdited`: A date/time field set to the date this record was last edited through `write()` +- `ID`: Primary Key. This will use the database's built-in auto-numbering system on the base table, and apply the same ID to all subclass tables. +- `ClassName`: An enumeration listing this data-class and all of its subclasses. The value is the actual `DataObject` subclass used to write the record. +- `Created`: A date/time field set to the creation date (i.e. when it was first written to the database) of this record +- `LastEdited`: A date/time field set to the date this record was last edited through `write()` The table creation SQL statement for our `Player` model above looks like this: @@ -104,7 +105,7 @@ CREATE TABLE `Player` ( ); ``` -## Creating Data Records +## Creating data records A new instance of a [DataObject](api:SilverStripe\ORM\DataObject) can be created using the `new` keyword. @@ -121,14 +122,14 @@ $player = Player::create(); [hint] Using the `create()` method provides chainability (known as a "fluent API" or "[fluent interface](https://en.wikipedia.org/wiki/Fluent_interface)"), which can add elegance and brevity to your code, e.g. `Player::create(['FirstName' => 'Sam'])->write()`. -More importantly, however, it will look up the class in the [Injector](api:SilverStripe\Core\Injector\Injector) so that the class can be overridden by [dependency injection](../extending/injector). For this reason, instantiating records using the `new` keyword is considered bad practice. +More importantly, however, it will look up the class in the [`Injector`](api:SilverStripe\Core\Injector\Injector) so that the class can be overridden by [dependency injection](../extending/injector). For this reason, instantiating records using the `new` keyword is considered bad practice. [/hint] Database columns (aka fields) can be set as class properties on the object. The Silverstripe CMS ORM handles the saving of the values through a custom `__set()` method. ```php -$player->FirstName = "Sam"; +$player->FirstName = 'Sam'; $player->PlayerNumber = 07; ``` @@ -146,7 +147,7 @@ $player = Player::create(); $id = $player->write(); ``` -## Querying Data +## Querying data With the `Player` class defined we can query our data using the ORM. The ORM provides shortcuts and methods for fetching, sorting and filtering data from our database. @@ -210,7 +211,7 @@ $timeSinceLastEdit = $player->dbObject('LastEdited')->Ago(); All database fields have a default value format which you can retrieve by treating the field as a class property - but there are also other formats available depending on the field type. You'll learn more about those in the [Data Types and Casting](data_types_and_casting) section - but the important thing to know is that you can get the `DBField` instance for a field by calling `dbObject('FieldName')` on the record - and that `DBField` instance will have different methods on it depending on the data type that let you access different formats of the field's value. -## Lazy Loading +## Lazy loading The ORM doesn't actually execute the [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) query until you iterate on the result (e.g. with a `foreach()` or `<% loop %>`). @@ -223,7 +224,7 @@ result set in PHP. In `MySQL` the query generated by the ORM may look something ```php $players = Player::get()->filter([ - 'FirstName' => 'Sam' + 'FirstName' => 'Sam', ]); $players = $players->sort('Surname'); @@ -236,7 +237,7 @@ This also means that getting the count of a list of objects will be done with a ```php $players = Player::get()->filter([ - 'FirstName' => 'Sam' + 'FirstName' => 'Sam', ])->sort('Surname'); // This will create an single SELECT COUNT query @@ -251,7 +252,7 @@ echo $players->Count(); ```php $players = Player::get(); -foreach($players as $player) { +foreach ($players as $player) { echo $player->FirstName; } ``` @@ -299,7 +300,7 @@ However you might have several entries with the same `FirstName` and would like ```php $players = Players::get()->sort([ 'FirstName' => 'ASC', - 'LastName' => 'ASC' + 'LastName' => 'ASC', ]); ``` @@ -309,7 +310,7 @@ You can also sort randomly. $players = Player::get()->shuffle(); ``` -## Filtering Results +## Filtering results The `filter()` method filters the list of objects that gets returned. @@ -368,7 +369,7 @@ $players = Player::get()->filter([ ]); ``` -### filterAny +### `filterAny` Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive), @@ -457,7 +458,7 @@ $teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300); The above examples are using "dot notation" to get the aggregations of the `Players` relation on the `Teams` model. See [Relations between Records](relations) to learn more. [/hint] -### filterByCallback +### `filterByCallback` It is possible to filter by a PHP callback using the [`filterByCallback()`](api:SilverStripe\ORM\DataList::filterByCallback()) method. This will force the data model to fetch all records and loop them in PHP which will be much worse for performance, thus `filter()` or `filterAny()` are to be preferred over `filterByCallback()`. @@ -474,14 +475,14 @@ for each record. The callback must return a boolean value. If the callback retur The below example will get all `Player` records aged over 10. ```php -$players = Player::get()->filterByCallback(function($record, $list) { +$players = Player::get()->filterByCallback(function ($record, $list) { return ($record->Age() > 10); }); ``` -### Exclude +### `exclude` -The [`exclude()`](api:SilverStripe\ORM\DataList::exclude()) method is the opposite to `filter()` in that it determines which entries to _exclude_ from a list, where `filter()` determines which to _include_. +The [`exclude()`](api:SilverStripe\ORM\DataList::exclude()) method is the opposite to `filter()` in that it determines which entries to *exclude* from a list, where `filter()` determines which to *include*. ```php // SELECT * FROM Player WHERE FirstName != 'Sam' @@ -492,7 +493,7 @@ Exclude both Sam and Sig. ```php $players = Player::get()->exclude([ - 'FirstName' => ['Sam', 'Sig'] + 'FirstName' => ['Sam', 'Sig'], ]); ``` @@ -523,7 +524,7 @@ And removing any named "Sig" or "Sam" with that are either age 17 or 43. // SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43')); $players = Player::get()->exclude([ 'FirstName' => ['Sam', 'Sig'], - 'Age' => [17, 43] + 'Age' => [17, 43], ]); ``` @@ -532,11 +533,11 @@ You can use [SearchFilters](searchfilters) to add additional behavior to your `e ```php $players = Player::get()->exclude([ 'FirstName:EndsWith' => 'S', - 'PlayerNumber:LessThanOrEqual' => '10' + 'PlayerNumber:LessThanOrEqual' => '10', ]); ``` -### Subtract +### `subtract` You can subtract entries from a [DataList](api:SilverStripe\ORM\DataList) by passing in another DataList to `subtract()` @@ -555,7 +556,7 @@ use SilverStripe\Security\Member; $otherMembers = Member::get()->subtract($group->Members()); ``` -### Limit +### `limit` You can limit the amount of records returned in a DataList by using the `limit()` method. @@ -586,7 +587,7 @@ namespace SilverStripe\BannerManager; use SilverStripe\ORM\DataObject; -class BannerImage extends DataObject +class BannerImage extends DataObject { private static $table_name = 'BannerImage'; } @@ -603,15 +604,15 @@ equivalent version. Methods which return class names: -* [`tableClass($table)`](api:SilverStripe\ORM\DataObjectSchema::tableClass()) - Finds the class name for a given table. This also handles suffixed tables such as `Table_Live` (see [Versioning](versioning)). -* [`baseDataClass($class)`](api:SilverStripe\ORM\DataObjectSchema::baseDataClass()) - Returns the base data class for the given class. -* [`classForField($class, $field)`](api:SilverStripe\ORM\DataObjectSchema::classForField()) - Finds the specific class that directly holds the given field +- [`tableClass($table)`](api:SilverStripe\ORM\DataObjectSchema::tableClass()) - Finds the class name for a given table. This also handles suffixed tables such as `Table_Live` (see [Versioning](versioning)). +- [`baseDataClass($class)`](api:SilverStripe\ORM\DataObjectSchema::baseDataClass()) - Returns the base data class for the given class. +- [`classForField($class, $field)`](api:SilverStripe\ORM\DataObjectSchema::classForField()) - Finds the specific class that directly holds the given field Methods which return table names: -* [`tableName($class)`](api:SilverStripe\ORM\DataObjectSchema::tableName()) - Returns the table name for a given class or object. -* [`baseDataTable($class)`](api:SilverStripe\ORM\DataObjectSchema::baseDataTable()) - Returns the base data class for the given class. -* [`tableForField($class, $field)`](api:SilverStripe\ORM\DataObjectSchema::tableForField()) - Finds the specific class that directly holds the given field and returns the table. +- [`tableName($class)`](api:SilverStripe\ORM\DataObjectSchema::tableName()) - Returns the table name for a given class or object. +- [`baseDataTable($class)`](api:SilverStripe\ORM\DataObjectSchema::baseDataTable()) - Returns the base data class for the given class. +- [`tableForField($class, $field)`](api:SilverStripe\ORM\DataObjectSchema::tableForField()) - Finds the specific class that directly holds the given field and returns the table. Note that in cases where the class name is required, an instance of the object may be substituted. @@ -619,20 +620,20 @@ For example, if running a query against a particular model, you will need to ens table and column. ```php -use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\Queries\SQLSelect; -public function countDuplicates($model, $fieldToCheck) +public function countDuplicates($model, $fieldToCheck) { $table = DataObject::getSchema()->tableForField($model, $field); - $query = new SQLSelect(); + $query = SQLSelect::create(); $query->setFrom("\"{$table}\""); $query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]); return $query->count(); } ``` -### Common Table Expressions (CTEs aka the `WITH` clause) {#cte} +### Common table expressions (CTE aka the `WITH` clause) {#cte} Common Table Expressions are a powerful tool both for optimising complex queries, and for creating recursive queries. You can use these by calling the [`DataQuery::with()`](api:SilverStripe\ORM\DataQuery::with()) method. @@ -645,9 +646,9 @@ The following example is the equivalent to the example in the [SQL Queries](/dev ```php use App\Model\ObjectWithParent; use SilverStripe\Core\Convert; +use SilverStripe\ORM\DB; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataQuery; -use SilverStripe\ORM\DB; // Only use the CTE functionality if it is supported by the current database if (DB::get_conn()->supportsCteQueries(true)) { @@ -685,9 +686,9 @@ The PHPDoc for the [`DataQuery::with()`](api:SilverStripe\ORM\DataQuery::with()) There are a few things that might catch you off guard with this abstraction if you aren't looking for them. Many of these are specifically enforced by MySQL and may not apply to other databases. -* `DataQuery` wants to use `DISTINCT` and to apply a sort order by default. MySQL 8 doesn't support `ORDER BY`, `DISTINCT`, or `LIMIT` in the recursive query block of Common Table Expressions so we need to make sure to explicitly set the sort order to null and distinct to false when using a `DataQuery` for that part of the query. -* If you use `DataQuery` for `$cteQuery` (i.e. the `$query` argument of the `with()` method), you can reduce the fields being selected by including them in the `$cteFields` argument. Be aware though that the number of fields passed in must match the number used in the recursive query if your CTE is recursive. - * `$cteFields` will be used to set the select fields for the `$cteQuery` if it's a `DataQuery` - but if it's a `SQLSelect` then this argument works the same as it does with `SQLSelect::addWith()`. +- `DataQuery` wants to use `DISTINCT` and to apply a sort order by default. MySQL 8 doesn't support `ORDER BY`, `DISTINCT`, or `LIMIT` in the recursive query block of Common Table Expressions so we need to make sure to explicitly set the sort order to null and distinct to false when using a `DataQuery` for that part of the query. +- If you use `DataQuery` for `$cteQuery` (i.e. the `$query` argument of the `with()` method), you can reduce the fields being selected by including them in the `$cteFields` argument. Be aware though that the number of fields passed in must match the number used in the recursive query if your CTE is recursive. + - `$cteFields` will be used to set the select fields for the `$cteQuery` if it's a `DataQuery` - but if it's a `SQLSelect` then this argument works the same as it does with `SQLSelect::addWith()`. ### Raw SQL @@ -730,47 +731,49 @@ You can specify an ORDER BY clause fragment with the `orderBy` method: $members = Member::get()->orderBy(/* some raw SQL here */); ``` -##### Joining Tables +##### Joining tables You can specify a join with the `innerJoin`, `leftJoin`, and `rightJoin` methods. All of these methods have the same arguments: -* The name of the table to join to. -* The filter clause for the join. -* An optional alias. +- The name of the table to join to. +- The filter clause for the join. +- An optional alias. ```php // Without an alias $members = Member::get() - ->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); + ->leftJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"'); $members = Member::get() - ->rightJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); + ->rightJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"'); $members = Member::get() - ->innerJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); + ->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"'); // With an alias "Rel" $members = Member::get() - ->leftJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); + ->leftJoin("Group_Members", '"Rel"."MemberID" = "Member"."ID"', "Rel"); $members = Member::get() - ->rightJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); + ->rightJoin("Group_Members", '"Rel"."MemberID" = "Member"."ID"', "Rel"); $members = Member::get() - ->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); + ->innerJoin("Group_Members", '"Rel"."MemberID" = "Member"."ID"', "Rel"); ``` [alert] -Using a join will _filter_ results further by the JOINs performed against the foreign table. It will +Using a join will *filter* results further by the JOINs performed against the foreign table. It will **not return** the additionally joined data. For the examples above, we're still only selecting values for the fields on the `Member` class table. [/alert] -### Default Values +### Default values Define the default values for all the `$db` fields. This example sets the `Status` column on Player to "Active" whenever a new record is created. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { // ... private static $defaults = [ @@ -790,21 +793,31 @@ time. For example, suppose we have the following set of classes: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Product extends DataObject +class Product extends DataObject { private static $table_name = 'Product'; private static $db = [ - 'SKU' => 'Text' + 'SKU' => 'Text', ]; } +``` + +```php +namespace App\Model; class DigitalProduct extends Product { private static $table_name = 'Product_Digital'; } +``` + +```php +namespace App\Model; class Computer extends DigitalProduct { @@ -839,36 +852,36 @@ Accessing the data is transparent to the developer. ```php $products = Computer::get(); -foreach($products as $product) { +foreach ($products as $product) { echo $product->SKU; } ``` The way the ORM stores the data is this: -* "Base classes" are direct sub-classes of [DataObject](api:SilverStripe\ORM\DataObject). They are always given a table, whether or not they declare +- "Base classes" are direct sub-classes of [DataObject](api:SilverStripe\ORM\DataObject). They are always given a table, whether or not they declare their own fields. This is called the "base table". In our case, `Product` is the base table. -* The base table's `ClassName` field is set to class of the given record. The column is an enumeration of all +- The base table's `ClassName` field is set to class of the given record. The column is an enumeration of all subclasses of the base class (including the base class itself). -* Each subclass of the base object will also be given its own table *as long as it has custom fields*. In the +- Each subclass of the base object will also be given its own table *as long as it has custom fields*. In the example above, `DigitalProduct` didn't define any new fields, so an extra table would be redundant. -* In all the tables, `ID` is the primary key. A matching `ID` number is used for all parts of a particular record: +- In all the tables, `ID` is the primary key. A matching `ID` number is used for all parts of a particular record: record #2 in the `Product` table refers to the same object as record #2 in the `Product_Digital_Computer` table. To retrieve a `Computer` record, Silverstripe CMS joins the `Product` and `Product_Digital_Computer` tables by their `ID` columns. -## Related Lessons -* [Introduction to the ORM](https://www.silverstripe.org/learn/lessons/v4/introduction-to-the-orm-1) -* [Adding custom fields to a page](https://www.silverstripe.org/learn/lessons/v4/adding-custom-fields-to-a-page-1) +## Related lessons +- [Introduction to the ORM](https://www.silverstripe.org/learn/lessons/v4/introduction-to-the-orm-1) +- [Adding custom fields to a page](https://www.silverstripe.org/learn/lessons/v4/adding-custom-fields-to-a-page-1) -## Related Documentation +## Related documentation -* [Data Types and Casting](/developer_guides/model/data_types_and_casting) +- [Data Types and Casting](/developer_guides/model/data_types_and_casting) -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) -* [DataList](api:SilverStripe\ORM\DataList) -* [DataQuery](api:SilverStripe\ORM\DataQuery) -* [DataObjectSchema](api:SilverStripe\ORM\DataObjectSchema) +- [DataObject](api:SilverStripe\ORM\DataObject) +- [DataList](api:SilverStripe\ORM\DataList) +- [DataQuery](api:SilverStripe\ORM\DataQuery) +- [DataObjectSchema](api:SilverStripe\ORM\DataObjectSchema) diff --git a/en/02_Developer_Guides/00_Model/02_Relations.md b/en/02_Developer_Guides/00_Model/02_Relations.md index 2cd8e2bdd..61995037a 100644 --- a/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/en/02_Developer_Guides/00_Model/02_Relations.md @@ -4,7 +4,7 @@ summary: Relate models together using the ORM using has_one, has_many, and many_ icon: link --- -# Relations between Records +# Relations between records In most situations you will likely see more than one [`DataObject`](api:SilverStripe\ORM\DataObject) and several classes in your data model may relate to one another. An example of this is a `Player` object may have a relationship to one or more `Team` or `Coach` classes @@ -13,11 +13,13 @@ and could take part in many `Games`. Relations are a key part of designing and b Relations are built through static array definitions on a class, in the format ` => `. Silverstripe CMS supports a number of relationship types and each relationship type can have any number of relations. -## has_one +## `has_one` Many-to-one and one-to-one relationships create a database-column called `ID`, in the example below this would be `TeamID` on the `Player` table. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Player extends DataObject @@ -25,17 +27,25 @@ class Player extends DataObject private static $has_one = [ 'Team' => Team::class, ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $db = [ - 'Title' => 'Varchar' + 'Title' => 'Varchar', ]; private static $has_many = [ 'Players' => Player::class, ]; + // ... } ``` @@ -46,16 +56,19 @@ and provides a short syntax for accessing the related object. Relations don't only apply to your own `DataObject` models - you can make relations to core models such as `File` and `Image` as well: ```php -use SilverStripe\ORM\DataObject; -use SilverStripe\Assets\Image; +namespace App\Model; + use SilverStripe\Assets\File; +use SilverStripe\Assets\Image; +use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $has_one = [ 'Teamphoto' => Image::class, - 'Lineup' => File::class - ]; + 'Lineup' => File::class, + ]; + // ... } ``` @@ -74,7 +87,7 @@ echo $player->Team()->Title; ``` [info] -Even if the `$player` record doesn't have any team record saved in its `Team` relation, `$player->Team()` will return a `Team` object. In that case, it will be an _empty_ record, with only default values applied. You can validate if that is the case by calling [`exists()`](api:SilverStripe\ORM\DataObject::exists()) on the record (e.g. `$player->Team()->exists()`). +Even if the `$player` record doesn't have any team record saved in its `Team` relation, `$player->Team()` will return a `Team` object. In that case, it will be an *empty* record, with only default values applied. You can validate if that is the case by calling [`exists()`](api:SilverStripe\ORM\DataObject::exists()) on the record (e.g. `$player->Team()->exists()`). [/info] The relationship can also be navigated in [templates](../templates). @@ -87,7 +100,7 @@ The relationship can also be navigated in [templates](../templates). <% end_with %> ``` -### Polymorphic has_one +### Polymorphic `has_one` {#polymorphic-has-one} A `has_one` relation can also be polymorphic, which allows any type of object to be associated. This is useful where there could be many use cases for a particular data structure. @@ -95,24 +108,41 @@ This is useful where there could be many use cases for a particular data structu An additional column is created called `Class`, which along with the `ID` column identifies the object. -To specify that a `has_one` relation is polymorphic set the type to [api:SilverStripe\ORM\DataObject]. +To specify that a `has_one` relation is polymorphic set the type to [`DataObject`](api:SilverStripe\ORM\DataObject). Ideally, the associated `has_many` (or `belongs_to`) should be specified with ["dot notation"](#dot-notation). ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Player extends DataObject { private static $has_many = [ - 'Fans' => Fan::class.'.FanOf', + 'Fans' => Fan::class . '.FanOf', ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class Team extends DataObject { private static $has_many = [ - 'Fans' => Fan::class.'.FanOf', + 'Fans' => Fan::class . '.FanOf', ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Fan extends DataObject { @@ -121,6 +151,7 @@ class Fan extends DataObject private static $has_one = [ 'FanOf' => DataObject::class, ]; + // ... } ``` @@ -128,17 +159,17 @@ class Fan extends DataObject Note: The use of polymorphic relationships can affect query performance, especially on joins, and also increases the complexity of the database and necessary user code. They should be used sparingly, and only where additional complexity would otherwise -be necessary. E.g. Additional parent classes for each respective relationship, or +be necessary. For example additional parent classes for each respective relationship, or duplication of code. [/warning] -### belongs_to +### `belongs_to` Defines a one-to-one relationship with another object, which declares the other end of the relationship with a corresponding `has_one`. A single database column named `ID` will be created in the object with the `has_one`, but the `belongs_to` by itself will not create a database field. -Similarly with `has_many` below, [dot notation](#dot-notation) can (and for best practice _should_) be used to explicitly specify the `has_one` which refers to this relation. +Similarly with `has_many` below, [dot notation](#dot-notation) can (and for best practice *should*) be used to explicitly specify the `has_one` which refers to this relation. This is not mandatory unless the relationship would be otherwise ambiguous. [hint] @@ -146,29 +177,37 @@ You can use `RelationValidationService` for validation of relationships. This to [/hint] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $has_one = [ - 'Coach' => Coach::class + 'Coach' => Coach::class, ]; } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Coach extends DataObject { private static $belongs_to = [ - 'Team' => Team::class.'.Coach' + 'Team' => Team::class . '.Coach', ]; } ``` -## has_many +## `has_many` Defines one-to-many joins. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`. [alert] -When defining a `has_many` relation, you _must_ specify a `has_one` relationship on the related class as well. To add a `has_one` relation on core classes, yml config settings can be used: +When defining a `has_many` relation, you *must* specify a `has_one` relationship on the related class as well. To add a `has_one` relation on core classes, yml config settings can be used: ```yml SilverStripe\Assets\Image: @@ -180,6 +219,8 @@ Note that in some cases you may be better off using a `many_many` relation inste [/alert] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject @@ -191,13 +232,21 @@ class Team extends DataObject private static $has_many = [ 'Players' => Player::class, ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Player extends DataObject { private static $has_one = [ 'Team' => Team::class, ]; + // ... } ``` @@ -205,8 +254,6 @@ Much like the `has_one` relationship, `has_many` can be navigated through the OR you will get an instance of [`HasManyList`](api:SilverStripe\ORM\HasManyList) rather than the object. ```php -use SilverStripe\ORM\HasManyList; - $team = Team::get()->first(); /** @var HasManyList $players */ @@ -215,7 +262,7 @@ $players = $team->Players(); /** @var int $numPlayers */ $numPlayers = $players->Count(); -foreach($players as $player) { +foreach ($players as $player) { echo $player->FirstName; } ``` @@ -223,6 +270,10 @@ foreach($players as $player) { If you're using the default scaffolded form fields with multiple `has_one` relationships, you will end up with a CMS field for each relation. If you don't want these you can remove them, referring to them with as `ID`: ```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class Company extends DataObject { // ... @@ -241,15 +292,24 @@ class Company extends DataObject To specify multiple `has_many`, `many_many`, `belongs_to`, or `belongs_many_many` relationships to the same model class (and as a general best practice) you can use dot notation to distinguish them like below: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Person extends DataObject { private static $has_many = [ - 'Managing' => Company::class.'.Manager', - 'Cleaning' => Company::class.'.Cleaner', + 'Managing' => Company::class . '.Manager', + 'Cleaning' => Company::class . '.Cleaner', ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Company extends DataObject { @@ -257,16 +317,17 @@ class Company extends DataObject 'Manager' => Person::class, 'Cleaner' => Person::class, ]; + // ... } ``` -Multiple `has_many` or `belongs_to` relationships are okay _without_ dot notation if they aren't linking to the same model class. Otherwise, using dot notation is required. With that said, dot notation is recommended in all cases as it makes your code more resilient to change. Adding new relationships is easier when you don't need to review and update existing ones. +Multiple `has_many` or `belongs_to` relationships are okay *without* dot notation if they aren't linking to the same model class. Otherwise, using dot notation is required. With that said, dot notation is recommended in all cases as it makes your code more resilient to change. Adding new relationships is easier when you don't need to review and update existing ones. [hint] You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. [/hint] -## many_many relationships {#many-many} +## `many_many` relationships {#many-many} [warning] Please specify a `belongs_many_many` relationship on the related class as well in order to have the necessary accessors available on both ends. See [`belongs_many_many`](#belongs-many-many) for more information. @@ -280,7 +341,7 @@ There are two ways this relationship can be declared which are (described below) You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. [/hint] -### Automatic many_many table +### Automatic `many_many` table {#automatic-many-many-table} If you specify only a single class as the other side of the many-many relationship, then a table will be automatically created between the two. It will be the table name of the class which declares the `many_many` relationship, suffixed with the relationship name (e.g. `Team_Supporters`). @@ -290,6 +351,8 @@ Extra fields on the mapping table can be created by declaring a `many_many_extra config to add extra columns. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject @@ -300,40 +363,49 @@ class Team extends DataObject private static $many_many_extraFields = [ 'Supporters' => [ - 'Ranking' => 'Int' - ] + 'Ranking' => 'Int', + ], ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Supporter extends DataObject { private static $belongs_many_many = [ 'Supports' => Team::class, ]; + // ... } ``` To ensure this `many_many` is sorted by "Ranking" by default you can add this to your config: -```yaml +```yml Team_Supporters: default_sort: 'Ranking ASC' ``` `Team_Supporters` is the table name automatically generated for the `many_many` relation in this case. -### many_many through relationship joined on a separate DataObject {#many-many-through} +### `many_many` through relationship joined on a separate `DataObject` {#many-many-through} If necessary, a third `DataObject` class can instead be specified as the joining table, rather than having the ORM generate an automatically scaffolded table. This has the following advantages: - Allows versioning of the mapping table, including support for the -ownership api (see [Versioning](/developer_guides/model/versioning) for more information). +ownership API (see [Versioning](/developer_guides/model/versioning) for more information). - Allows support of other extensions on the mapping table (e.g. for [subsites](https://github.com/silverstripe/silverstripe-subsites) or localisation via [fluent](https://github.com/tractorcow-farm/silverstripe-fluent)). - Extra fields can easily be managed separately via the joined dataobject, even via a separate `GridField` or form. This is declared via array syntax, with the following keys on the `many_many` relation: + - `through`: Class name of the mapping table - `from`: Name of the `has_one` relationship pointing back at the object declaring `many_many` - `to`: Name of the `has_one` relationship pointing to the object declaring `belongs_many_many`. @@ -347,18 +419,27 @@ or child record. The [syntax for `belongs_many_many`](#belongs-many-many) is unchanged. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $many_many = [ - "Supporters" => [ + 'Supporters' => [ 'through' => TeamSupporter::class, 'from' => 'Team', 'to' => 'Supporter', - ] + ], ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Supporter extends DataObject { @@ -367,7 +448,14 @@ class Supporter extends DataObject private static $belongs_many_many = [ 'Supports' => Team::class, ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class TeamSupporter extends DataObject { @@ -395,10 +483,11 @@ $supporters = $team->Supporters()->filter(['Ranking' => 1]); For records accessed in a [`ManyManyThroughList`](api:SilverStripe\ORM\ManyManyThroughList), you can access the join record (e.g. for our example above a `TeamSupporter` instance) by calling [`getJoin()`](api:SilverStripe\ORM\DataObject::getJoin()) or as the `$Join` property in templates. [/hint] -#### Polymorphic many_many +#### Polymorphic `many_many` Using many_many through it is possible to support polymorphic relations on the mapping table. Note, that this feature has certain limitations: + - This feature only works with many_many through - This feature will only allow polymorphic `many_many`, but not `belongs_many_many`. - You can have a `has_many` relation to the join table where you would normally use `belongs_many_many`, and iterate through it @@ -409,6 +498,8 @@ Note that this works by leveraging a polymorphic `has_one` relation on the join For instance, this is how you would link an arbitrary object to `many_many` tags. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class SomeObject extends DataObject @@ -419,9 +510,16 @@ class SomeObject extends DataObject 'through' => TagMapping::class, 'from' => 'Parent', 'to' => 'Tag', - ] + ], ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Tag extends DataObject { @@ -436,33 +534,37 @@ class Tag extends DataObject * This is a list of arbitrary types of objects * @return Generator */ - public function TaggedObjects() + public function getTaggedObjects() { foreach ($this->TagMappings() as $mapping) { yield $mapping->Parent(); } } } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class TagMapping extends DataObject -{ +{ private static $has_one = [ - 'Parent' => DataObject::class, // Polymorphic has_one + // Polymorphic has_one + 'Parent' => DataObject::class, 'Tag' => Tag::class, ]; } ``` -### Using many_many relationships +### Using `many_many` relationships Much like `has_one` and `has_many` relationships, `many_many` can be navigated through the ORM as well. The only difference being you will get an instance of [ManyManyList](api:SilverStripe\ORM\ManyManyList) or [ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) returned. ```php -use SilverStripe\ORM\ManyManyList; -use SilverStripe\ORM\ManyManyThroughList; - $team = Team::get()->byId(1); /** @var MayManyList|ManyManyThroughList $supporters */ @@ -508,7 +610,7 @@ $joinRecord->Ranking = 2; $joinRecord->write(); ``` -#### Using many_many in templates +#### Using `many_many` in templates The relationship can also be navigated in [templates](../templates). @@ -534,9 +636,10 @@ This also provides three ways to access the extra fields on a many_many through <% end_loop %> <% end_with %> ``` + [/hint] -## belongs_many_many +## `belongs_many_many` {#belongs-many-many} The `belongs_many_many` relation represents the other side of the `many_many` relationship. When using either a basic `many_many` or a `many_many` through, the syntax for `belongs_many_many` is the same. @@ -545,6 +648,8 @@ To specify multiple `many_many` relationships between the same classes, specify distinguish them like below: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Category extends DataObject @@ -553,14 +658,22 @@ class Category extends DataObject 'Products' => Product::class, 'FeaturedProducts' => Product::class, ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Product extends DataObject -{ +{ private static $belongs_many_many = [ - 'Categories' => Category::class.'.Products', - 'FeaturedInCategories' => Category::class.'.FeaturedProducts', + 'Categories' => Category::class . '.Products', + 'FeaturedInCategories' => Category::class . '.FeaturedProducts', ]; + // ... } ``` @@ -575,6 +688,10 @@ more likely that the user will select categories for a product than vice-versa. Querying nested relationships inside a loop using the ORM is prone to the N + 1 query problem. To illustrate the N + 1 query problem, imagine a scenario where there are Teams with many child Players ```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class Team extends DataObject { private static $has_many = [ @@ -628,6 +745,10 @@ SELECT * FROM Player WHERE TeamID IN (1, 2, 3, ...) Suppose we have the following related classes: ```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class Team extends DataObject { private static $has_many = [ @@ -635,6 +756,12 @@ class Team extends DataObject 'Fans' => Fan::class, ]; } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Player extends DataObject { @@ -646,6 +773,12 @@ class Player extends DataObject 'Games' => Game::class, ]; } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class Game extends DataObject { @@ -688,7 +821,7 @@ You can get the results for multiple nested relations with multiple arguments: ```php $teams = Team::get()->eagerLoad( 'Players.Games.Officials', - 'Players.Games.Sponsors', + 'Players.Games.Sponsors' ); ``` @@ -723,6 +856,8 @@ Built-in controllers using delete operations check `canDelete()` on the owner, b [/alert] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class ParentObject extends DataObject @@ -734,6 +869,7 @@ class ParentObject extends DataObject private static $cascade_deletes = [ 'Child', ]; + // ... } ``` @@ -746,7 +882,7 @@ on a parent object will trigger unpublish on the child, similarly to how `owns` See the [versioning docs](/developer_guides/model/versioning) for more information on ownership. [alert] -If the child model is not versioned, `cascade_deletes` will result in the child record being _deleted_ if the parent is unpublished! Be sure to check whether both sides of the relationship are versioned before declaring `cascade_deletes`. +If the child model is not versioned, `cascade_deletes` will result in the child record being *deleted* if the parent is unpublished! Be sure to check whether both sides of the relationship are versioned before declaring `cascade_deletes`. [/alert] ## Cascading duplications @@ -759,12 +895,14 @@ and saved against the new clone object. Note that duplications will act differently depending on the kind of relation: - Records in one-to-many or many-to-many relationships (e.g. `has_many`, `has_one`, and `belongs_to`) will be explicitly duplicated. -- Records in many-to-many (i.e. `many_many` and `belongs_many_many`) relationships will _not_ be duplicated, but the mapping table values will instead +- Records in many-to-many (i.e. `many_many` and `belongs_many_many`) relationships will *not* be duplicated, but the mapping table values will instead be copied so that the original records are related to the new duplicate record. For example: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class ParentObject extends DataObject @@ -805,7 +943,8 @@ $parent = ParentObject::get()->first(); // Only duplicate the `$parent` record $dupe = $parent->duplicate(relations: false); -// Duplicate the `$parent` record, and cascade duplicate the "Children" relation (ignoring any cascade_duplicates configuration) +// Duplicate the `$parent` record, and cascade duplicate the "Children" relation +// (ignoring any cascade_duplicates configuration) $dupe = $parent->duplicate(relations: ['Children']); ``` @@ -823,8 +962,8 @@ and `remove()` method). $team = Team::get()->byId(1); // create a new supporter -$supporter = new Supporter(); -$supporter->Name = "Foo"; +$supporter = Supporter::create(); +$supporter->Name = 'Foo'; $supporter->write(); // add the supporter. @@ -839,7 +978,7 @@ To set what record is in a `has_one` relation, just set the `ID` Don't forget to write the record (`$player->write();`)! [/hint] -## Custom Relations +## Custom relations You can use the ORM to get a filtered result list without writing any SQL. For example, this snippet gets you the `Players` relation on a team, but only containing active players. @@ -847,20 +986,22 @@ You can use the ORM to get a filtered result list without writing any SQL. For e See [Filtering Results](/developer_guides/model/data_model_and_orm/#filtering-results) for more information. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $has_many = [ - 'Players' => Player::class + 'Players' => Player::class, ]; - public function ActivePlayers() + public function getActivePlayers() { return $this->Players()->filter('Status', 'Active'); } + // ... } - ``` [notice] @@ -876,9 +1017,10 @@ $playersList->add($newPlayer); // Still returns 'Inactive' $status = $newPlayer->Status; ``` + [/notice] -## Relations on Unsaved Objects +## Relations on unsaved objects You can also set `has_many` and `many_many` relations before the `DataObject` is saved. This behavior uses the [UnsavedRelationList](api:SilverStripe\ORM\UnsavedRelationList) and converts it into the correct [`RelationList`](api:SilverStripe\ORM\RelationList) subclass when saving the `DataObject` for the first @@ -894,16 +1036,16 @@ for displaying the objects contained in the relation. The [`RelationValidationService`](api:SilverStripe\Dev\Validation\RelationValidationService) can be used to check if your relations are set up according to best practices, and is very useful for debugging unexpected behaviour with relations. It is disabled by default. -To enable this service, set the following yaml configuration, which will give you validation output every time you run `dev/build`. +To enable this service, set the following YAML configuration, which will give you validation output every time you run `dev/build`. -```yaml +```yml SilverStripe\Dev\Validation\RelationValidationService: output_enabled: true ``` By default, this service only inspects relations for classes which have either no namespace or a namespace beginning with `App\`. You can declare your own namespace prefixes by setting the `allow_rules` configuration: -```yaml +```yml SilverStripe\Dev\Validation\RelationValidationService: allow_rules: # using the "app" key you can override the default "App" namespace @@ -917,7 +1059,7 @@ SilverStripe\Dev\Validation\RelationValidationService: You can also tell the service to ignore classes whose namespace starts a certain way: -```yaml +```yml SilverStripe\Dev\Validation\RelationValidationService: deny_rules: - 'MyApp\SpecialCases' @@ -925,7 +1067,7 @@ SilverStripe\Dev\Validation\RelationValidationService: If you have relations that you've intentionally set up in a way that the validation service warns against, you can tell it to ignore those specific relations by setting `deny_relations` config. Syntax for this is `.`. -```yaml +```yml SilverStripe\Dev\Validation\RelationValidationService: deny_relations: - 'App\Model\Player.Teams' @@ -949,26 +1091,24 @@ use SilverStripe\Dev\Validation\RelationValidationService; $messages = RelationValidationService::singleton()->inspectClasses([Team::class, Player::class]); ``` -## Link Tracking +## Link tracking You can control the visibility of the `Link Tracking` tab by setting the `show_sitetree_link_tracking` config. This defaults to `false` for most `DataObject`'s. It is also possible to control the visibility of the `File Tracking` tab by setting the `show_file_link_tracking` config. -## Related Lessons -* [Working with data relationships -- has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) -* [Working with data relationships -- many_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1) - -## Related Documentation +## Related lessons -* [Introduction to the Data Model and ORM](data_model_and_orm) -* [Lists](lists) +- [Working with data relationships -- has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) +- [Working with data relationships -- many_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1) -## API Documentation +## Related documentation -* [HasManyList](api:SilverStripe\ORM\HasManyList) -* [ManyManyList](api:SilverStripe\ORM\ManyManyList) -* [ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) -* [DataObject](api:SilverStripe\ORM\DataObject) -* [LinkTracking](api:SilverStripe\CMS\Model\SiteTreeLinkTracking) +- [Introduction to the Data Model and ORM](data_model_and_orm) +- [Lists](lists) +- [HasManyList](api:SilverStripe\ORM\HasManyList) +- [ManyManyList](api:SilverStripe\ORM\ManyManyList) +- [ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) +- [DataObject](api:SilverStripe\ORM\DataObject) +- [LinkTracking](api:SilverStripe\CMS\Model\SiteTreeLinkTracking) diff --git a/en/02_Developer_Guides/00_Model/03_Lists.md b/en/02_Developer_Guides/00_Model/03_Lists.md index e20524c8f..e5a8d53f2 100644 --- a/en/02_Developer_Guides/00_Model/03_Lists.md +++ b/en/02_Developer_Guides/00_Model/03_Lists.md @@ -4,7 +4,7 @@ summary: The SS_List interface allows you to iterate through and manipulate a li icon: list --- -# Managing Lists +# Managing lists Whenever using the ORM to fetch records or navigate relationships you will receive an [SS_List](api:SilverStripe\ORM\SS_List) instance commonly as either [DataList](api:SilverStripe\ORM\DataList) or [RelationList](api:SilverStripe\ORM\RelationList). This object gives you the ability to iterate over each of the results or @@ -21,7 +21,7 @@ use SilverStripe\Security\Member; $members = Member::get(); -foreach($members as $member) { +foreach ($members as $member) { echo $member->Name; } ``` @@ -79,12 +79,13 @@ $emailAddresses = Member::get()->column('Email'); ## Iterating over a large list {#chunkedFetch} -When iterating over a `DataList`, the ORM will create a [`Generator`](https://www.php.net/manual/en/language.generators.overview.php). This means we don't have all of the `DataObject` records in the list instantiated in memory, but the ORM _does_ fetch all of the data about those records and loads that data in memory. This can consume a lot of memory when working with a large data set. +When iterating over a `DataList`, the ORM will create a [`Generator`](https://www.php.net/manual/en/language.generators.overview.php). This means we don't have all of the `DataObject` records in the list instantiated in memory, but the ORM *does* fetch all of the data about those records and loads that data in memory. This can consume a lot of memory when working with a large data set. To limit the amount of data loaded in memory, you can use the [`chunkedFetch()`](api:SilverStripe\ORM\DataList::chunkedFetch()) method on your `DataList`. In most cases, you can iterate over the results of `chunkedFetch()` the same way you would iterate over your `DataList`. Internally, `chunkedFetch()` will split the database query into smaller queries and keep running through them until it runs out of results. ```php -// Without using chunked fetch, all of the data for all of the Member records will be fetched from the database in a single query +// Without using chunked fetch, all of the data for all of the Member records will be fetched from the database +// in a single query $members = Member::get(); foreach ($members as $member) { echo $member->Email; @@ -113,9 +114,10 @@ foreach ($members as $member) { ``` There are some limitations: -* `chunkedFetch()` will ignore any limit or offset you have applied to your `DataList` -* you cannot "count" a chunked list or do any other call against it aside from iterating it -* while iterating over a chunked list, you cannot perform any operation that would alter the order of the items. + +- `chunkedFetch()` will ignore any limit or offset you have applied to your `DataList` +- you cannot "count" a chunked list or do any other call against it aside from iterating it +- while iterating over a chunked list, you cannot perform any operation that would alter the order of the items. ## ArrayList @@ -133,13 +135,14 @@ $list->push($sig); $numItems = $list->Count(); ``` -## Related Lessons -* [Lists and pagination](https://www.silverstripe.org/learn/lessons/v4/lists-and-pagination-1) +## Related lessons + +- [Lists and pagination](https://www.silverstripe.org/learn/lessons/v4/lists-and-pagination-1) -## API Documentation +## API documentation -* [SS_List](api:SilverStripe\ORM\SS_List) -* [RelationList](api:SilverStripe\ORM\RelationList) -* [DataList](api:SilverStripe\ORM\DataList) -* [ArrayList](api:SilverStripe\ORM\ArrayList) -* [Map](api:SilverStripe\ORM\Map) +- [SS_List](api:SilverStripe\ORM\SS_List) +- [RelationList](api:SilverStripe\ORM\RelationList) +- [DataList](api:SilverStripe\ORM\DataList) +- [ArrayList](api:SilverStripe\ORM\ArrayList) +- [Map](api:SilverStripe\ORM\Map) diff --git a/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md b/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md index 6cf397907..7542da3f2 100644 --- a/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md +++ b/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md @@ -4,7 +4,7 @@ summary: Learn how data is stored going in and coming out of the ORM and how to icon: code --- -# Data types and Casting +# Data types and casting Each model in a Silverstripe CMS [`DataObject`](api:SilverStripe\ORM\DataObject) will handle data at some point. This includes database columns such as the ones defined in a `$db` array and methods that return data for for use in templates. @@ -14,60 +14,61 @@ how to store its data in the database and how to format the information coming o In the `Player` example, we have four database columns each with a different data type (`Int`, `Varchar`, etc). -**app/src/Player.php** - ```php +// app/src/Player.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $db = [ 'PlayerNumber' => 'Int', 'FirstName' => 'Varchar(255)', 'LastName' => 'Text', - 'Birthday' => 'Date' + 'Birthday' => 'Date', ]; } ``` -## Available Types - -* `'BigInt'`: An 8-byte signed integer field (see: [DBBigInt](api:SilverStripe\ORM\FieldType\DBBigInt)). -* `'Boolean'`: A boolean field (see: [DBBoolean](api:SilverStripe\ORM\FieldType\DBBoolean)). -* `'Currency'`: A number with 2 decimal points of precision, designed to store currency values. Only supports single currencies (see: [DBCurrency](api:SilverStripe\ORM\FieldType\DBCurrency)). -* `'Date'`: A date field (see: [DBDate](api:SilverStripe\ORM\FieldType\DBDate)). -* `'Datetime'`: A date/time field (see: [DBDatetime](api:SilverStripe\ORM\FieldType\DBDatetime)). -* `'DBClassName'`: A special enumeration for storing class names (see: [DBClassName](api:SilverStripe\ORM\FieldType\DBClassName)). -* `'Decimal'`: A decimal number (see: [DBDecimal](api:SilverStripe\ORM\FieldType\DBDecimal)). -* `'Double'`: A floating point number with double precision (see: [DBDouble](api:SilverStripe\ORM\FieldType\DBDouble)). -* `'Enum'`: An enumeration of a set of strings that can store a single value (see: [DBEnum](api:SilverStripe\ORM\FieldType\DBEnum)). -* `'Float'`: A floating point number (see: [DBFloat](api:SilverStripe\ORM\FieldType\DBFloat)). -* `'Foreignkey'`: A special `Int` field used for foreign keys in `has_one` relationships (see: [DBForeignKey](api:SilverStripe\ORM\FieldType\DBForeignKey)). -* `'HTMLFragment'`: A variable-length string of up to 2MB, designed to store HTML. Doesn't process [shortcodes](/developer_guides/extending/shortcodes/). (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). -* `'HTMLText'`: A variable-length string of up to 2MB, designed to store HTML. Processes [shortcodes](/developer_guides/extending/shortcodes/). (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). -* `'HTMLVarchar'`: A variable-length string of up to 255 characters, designed to store HTML. Can process [shortcodes](/developer_guides/extending/shortcodes/) with additional configuration. (see: [DBHTMLVarchar](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)). -* `'Int'`: A 32-bit signed integer field (see: [DBInt](api:SilverStripe\ORM\FieldType\DBInt)). -* `'Locale'`: A field for storing locales (see: [DBLocale](api:SilverStripe\ORM\FieldType\DBLocale)). -* `'Money'`: Similar to Currency, but with localisation support (see: [DBMoney](api:SilverStripe\ORM\FieldType\DBMoney)). -* `'MultiEnum'`: An enumeration set of strings that can store multiple values (see: [DBMultiEnum](api:SilverStripe\ORM\FieldType\DBMultiEnum)). -* `'Percentage'`: A decimal number between 0 and 1 that represents a percentage (see: [DBPercentage](api:SilverStripe\ORM\FieldType\DBPercentage)). -* `'PolymorphicForeignKey'`: A special ForeignKey class that handles relations with arbitrary class types (see: [DBPolymorphicForeignKey](api:SilverStripe\ORM\FieldType\DBPolymorphicForeignKey)). -* `'PrimaryKey'`: A special type Int field used for primary keys. (see: [DBPrimaryKey](api:SilverStripe\ORM\FieldType\DBPrimaryKey)). -* `'Text'`: A variable-length string of up to 2MB, designed to store raw text (see: [DBText](api:SilverStripe\ORM\FieldType\DBText)). -* `'Time'`: A time field (see: [DBTime](api:SilverStripe\ORM\FieldType\DBTime)). -* `'Varchar'`: A variable-length string of up to 255 characters, designed to store raw text (see: [DBVarchar](api:SilverStripe\ORM\FieldType\DBVarchar)). -* `'Year'`: Represents a single year field (see: [DBYear](api:SilverStripe\ORM\FieldType\DBYear)). +## Available types + +- `'BigInt'`: An 8-byte signed integer field (see: [DBBigInt](api:SilverStripe\ORM\FieldType\DBBigInt)). +- `'Boolean'`: A boolean field (see: [DBBoolean](api:SilverStripe\ORM\FieldType\DBBoolean)). +- `'Currency'`: A number with 2 decimal points of precision, designed to store currency values. Only supports single currencies (see: [DBCurrency](api:SilverStripe\ORM\FieldType\DBCurrency)). +- `'Date'`: A date field (see: [DBDate](api:SilverStripe\ORM\FieldType\DBDate)). +- `'Datetime'`: A date/time field (see: [DBDatetime](api:SilverStripe\ORM\FieldType\DBDatetime)). +- `'DBClassName'`: A special enumeration for storing class names (see: [DBClassName](api:SilverStripe\ORM\FieldType\DBClassName)). +- `'Decimal'`: A decimal number (see: [DBDecimal](api:SilverStripe\ORM\FieldType\DBDecimal)). +- `'Double'`: A floating point number with double precision (see: [DBDouble](api:SilverStripe\ORM\FieldType\DBDouble)). +- `'Enum'`: An enumeration of a set of strings that can store a single value (see: [DBEnum](api:SilverStripe\ORM\FieldType\DBEnum)). +- `'Float'`: A floating point number (see: [DBFloat](api:SilverStripe\ORM\FieldType\DBFloat)). +- `'Foreignkey'`: A special `Int` field used for foreign keys in `has_one` relationships (see: [DBForeignKey](api:SilverStripe\ORM\FieldType\DBForeignKey)). +- `'HTMLFragment'`: A variable-length string of up to 2MB, designed to store HTML. Doesn't process [shortcodes](/developer_guides/extending/shortcodes/). (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). +- `'HTMLText'`: A variable-length string of up to 2MB, designed to store HTML. Processes [shortcodes](/developer_guides/extending/shortcodes/). (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). +- `'HTMLVarchar'`: A variable-length string of up to 255 characters, designed to store HTML. Can process [shortcodes](/developer_guides/extending/shortcodes/) with additional configuration. (see: [DBHTMLVarchar](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)). +- `'Int'`: A 32-bit signed integer field (see: [DBInt](api:SilverStripe\ORM\FieldType\DBInt)). +- `'Locale'`: A field for storing locales (see: [DBLocale](api:SilverStripe\ORM\FieldType\DBLocale)). +- `'Money'`: Similar to Currency, but with localisation support (see: [DBMoney](api:SilverStripe\ORM\FieldType\DBMoney)). +- `'MultiEnum'`: An enumeration set of strings that can store multiple values (see: [DBMultiEnum](api:SilverStripe\ORM\FieldType\DBMultiEnum)). +- `'Percentage'`: A decimal number between 0 and 1 that represents a percentage (see: [DBPercentage](api:SilverStripe\ORM\FieldType\DBPercentage)). +- `'PolymorphicForeignKey'`: A special ForeignKey class that handles relations with arbitrary class types (see: [DBPolymorphicForeignKey](api:SilverStripe\ORM\FieldType\DBPolymorphicForeignKey)). +- `'PrimaryKey'`: A special type Int field used for primary keys. (see: [DBPrimaryKey](api:SilverStripe\ORM\FieldType\DBPrimaryKey)). +- `'Text'`: A variable-length string of up to 2MB, designed to store raw text (see: [DBText](api:SilverStripe\ORM\FieldType\DBText)). +- `'Time'`: A time field (see: [DBTime](api:SilverStripe\ORM\FieldType\DBTime)). +- `'Varchar'`: A variable-length string of up to 255 characters, designed to store raw text (see: [DBVarchar](api:SilverStripe\ORM\FieldType\DBVarchar)). +- `'Year'`: Represents a single year field (see: [DBYear](api:SilverStripe\ORM\FieldType\DBYear)). See the [API documentation](api:SilverStripe\ORM\FieldType) for a full list of available data types. You can define your own [`DBField`](api:SilverStripe\ORM\FieldType\DBField) instances if required as well. -## Default Values for new database columns {#default-values} +## Default values for new database columns {#default-values} One way to define default values for new records is to use the `$defaults` configuration property, which is described in default in [Dynamic Default Values](how_tos/dynamic_default_fields). -That will only affect _new_ records, however. If you are adding a new field to an existing `DataObject` model, you may want to apply a default value for that field to _existing_ records as well. +That will only affect *new* records, however. If you are adding a new field to an existing `DataObject` model, you may want to apply a default value for that field to *existing* records as well. When adding a new `$db` field to a `DataObject`, you can specify a default value to be applied to all existing and new records when the column is added in the database -for the first time. You do this by passing an argument for the default value in your +for the first time. You do this by passing an argument for the default value in your `$db` items. For integer values, the default is the first parameter in the field specification. @@ -77,10 +78,12 @@ For enum values, it's the second parameter. For example: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Car extends DataObject -{ +class Car extends DataObject +{ private static $db = [ 'Wheels' => 'Int(4)', 'Condition' => 'Enum("New,Fair,Junk", "Fair")', @@ -93,7 +96,7 @@ class Car extends DataObject `Enum` fields will use the first defined value as the default if you don't explicitly declare one. In the example above, the default value would be "New" if it hadn't been declared. [/info] -## Formatting Output +## Formatting output The data type does more than set up the correct database schema. They can also define methods and formatting helpers for output. You can manually create instances of a data type and pass it through to the template. @@ -101,17 +104,18 @@ output. You can manually create instances of a data type and pass it through to In this case, we'll create a new method for our `Player` that returns the full name. By wrapping this in a [`DBVarchar`](api:SilverStripe\ORM\FieldType\DBVarchar) object we can control the formatting and it allows us to call methods defined from `Varchar` as `LimitCharacters`. -**app/src/Player.php** - ```php -use SilverStripe\ORM\FieldType\DBField; +// app/src/Model/Player.php +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\FieldType\DBField; -class Player extends DataObject +class Player extends DataObject { - public function getName() + public function getName() { - return DBField::create_field('Varchar', $this->FirstName . ' '. $this->LastName); + return DBField::create_field('Varchar', $this->FirstName . ' ' . $this->LastName); } } ``` @@ -119,6 +123,8 @@ class Player extends DataObject Then we can refer to a new `Name` column on our `Player` instances. In templates we don't need to use the `get` prefix. ```php +use App\Model\Player; + $player = Player::get()->byId(1); // returns the `DBVarChar` instance, which has the value "Sam Minnée" @@ -151,7 +157,7 @@ $player->Age + 5; $player->Age->value + 5; ``` -That doesn't apply to templates, where we can just treat all `DBField` instances as though they are primitives _or_ call methods on them: +That doesn't apply to templates, where we can just treat all `DBField` instances as though they are primitives *or* call methods on them: ```ss <% with $player %> @@ -161,6 +167,7 @@ That doesn't apply to templates, where we can just treat all `DBField` instances $Name.UpperCase <% end_with %> ``` + [/hint] On the most basic level, the `DBField` classes can be used for simple conversions from one value to another, e.g. to round a number. @@ -174,7 +181,7 @@ DBField::create_field('Double', 1.23456)->Round(2); Of course that's much more verbose than using the equivalent built-in PHP [`round()`](https://www.php.net/manual/en/function.round.php) function. The power of [DBField](api:SilverStripe\ORM\FieldType\DBField) comes with its more sophisticated helpers, like showing the time difference to the current date: ```php -use SilverStripe\ORM\FieldType\DBField;\ +use SilverStripe\ORM\FieldType\DBField; // returns "30 years ago" DBField::create_field('Date', '1982-01-01')->TimeDiff(); ``` @@ -185,27 +192,29 @@ Most objects in Silverstripe CMS extend from [ViewableData](api:SilverStripe\Vie context. Rather than manually returning objects from your custom functions. You can use the `$casting` configuration property. This casting only happens when you get the values in a template, so calling the method in your PHP code will always return the raw value. [hint] -While these examples are using `DataObject` subclasses, you can use the `$casting` configuration property on _any_ `ViewableData` subclass. +While these examples are using `DataObject` subclasses, you can use the `$casting` configuration property on *any* `ViewableData` subclass. [/hint] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $casting = [ 'Name' => 'Varchar', ]; - - public function getName() + + public function getName() { - return $this->FirstName . ' '. $this->LastName; + return $this->FirstName . ' ' . $this->LastName; } } ``` -Using this configuration, properties, fields, and methods on any Silverstripe CMS model can be type casted automatically in templates, by transforming its scalar value into an -instance of the [DBField](api:SilverStripe\ORM\FieldType\DBField) class, providing additional helpers. For example, a string can be cast as a [DBText](api:SilverStripe\ORM\FieldType\DBText) +Using this configuration, properties, fields, and methods on any Silverstripe CMS model can be type casted automatically in templates, by transforming its scalar value into an +instance of the [`DBField`](api:SilverStripe\ORM\FieldType\DBField) class, providing additional helpers. For example, a string can be cast as a [`DBText`](api:SilverStripe\ORM\FieldType\DBText) type, which has a `FirstSentence()` method to retrieve the first sentence in a longer piece of text. As mentioned above, this leaves you free to use the raw values in your PHP code while giving you all of the helper methods of the `DBField` instances in your templates. @@ -236,15 +245,17 @@ You can get the casted `DBField` instance of these properties by calling the [`o ```php $player = Player::get()->byId(1); -$player->getName(); // returns string -$player->Name; // returns string -$player->obj('Name'); // returns DBVarchar instance -$player->obj('Name')->LimitCharacters(2); // returns string +// returns string +$player->getName(); +// returns string +$player->Name; +// returns DBVarchar instance +$player->obj('Name'); +// returns string +$player->obj('Name')->LimitCharacters(2); ``` -## Casting HTML Text - -The database field types [`DBHTMLVarchar`](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)/[`DBHTMLText`](api:SilverStripe\ORM\FieldType\DBHTMLText) and [`DBVarchar`](api:SilverStripe\ORM\FieldType\DBVarchar)/[`DBText`](api:SilverStripe\ORM\FieldType\DBText) are exactly the same in +The database field types [`DBHTMLVarchar`](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)/[`DBHTMLText`](api:SilverStripe\ORM\FieldType\DBHTMLText) and [`DBVarchar`](api:SilverStripe\ORM\FieldType\DBVarchar)/[`DBText`](api:SilverStripe\ORM\FieldType\DBText) are exactly the same in the database. However, the template engine knows to escape the non-HTML variants automatically in templates, to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see [security](../security)), which is important if these fields store user-provided data. @@ -253,7 +264,7 @@ See the [Template casting](/developer_guides/templates/casting) section for cont ## Overriding -"Getters" and "Setters" are functions that help save fields to our [DataObject](api:SilverStripe\ORM\DataObject) instances. By default, the +"Getter" and "setter" methods help save fields to our [`DataObject`](api:SilverStripe\ORM\DataObject) instances. By default, the methods `getField()` and `setField()` are used to set column data. They save to the protected array, `$obj->record`. We can override the default behavior by making a method called "`get()`" or "`set()`". @@ -261,6 +272,8 @@ The following example will use the result of `getCost()` instead of the `Cost` d database column using `getField()`. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; /** @@ -270,7 +283,8 @@ class Product extends DataObject { private static $db = [ 'Title' => 'Varchar(255)', - 'Cost' => 'Int', //cost in pennies/cents + //cost in pennies/cents + 'Cost' => 'Int', ]; public function getCost() @@ -289,7 +303,7 @@ class Product extends DataObject Note that in the example above we've used a PHPDoc comment to indicate that the `$Cost` property is a `float`, even though the database field type is `Int`. This is because the `getCost()` getter method will automatically be used when trying to access `Cost` as a property (i.e. `$product->Cost` will return the result of `$product->getCost()`). [/hint] -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) -* [DBField](api:SilverStripe\ORM\FieldType\DBField) +- [DataObject](api:SilverStripe\ORM\DataObject) +- [DBField](api:SilverStripe\ORM\FieldType\DBField) diff --git a/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md b/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md index 4d92f5e3b..e195a4f63 100644 --- a/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md +++ b/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md @@ -3,18 +3,18 @@ title: Extending DataObjects summary: Modify the data model without using subclasses. --- -# Extending DataObjects +# Extending `DataObject` models -You can add properties and methods to existing [`DataObject`](api:SilverStripe\ORM\DataObject) subclasses like [`Member`](api:SilverStripe\Security\Member) without hacking core code or subclassing by using an [`Extension`](api:SilverStripe\Core\Extension). See the [Extending SilverStripe](../extending) guide for more information. +You can add properties and methods to existing [`DataObject`](api:SilverStripe\ORM\DataObject) subclasses like [`Member`](api:SilverStripe\Security\Member) without hacking core code or subclassing by using an [`Extension`](api:SilverStripe\Core\Extension). See the [Extending Silverstripe CMS](../extending) guide for more information. -The following documentation outlines some common hooks that the [Extension](api:SilverStripe\Core\Extension) API provides specifically for managing -data records. Note that this is _not_ an exhaustive list - we encourage you to look at the source code to see what other extension hooks are available. +The following documentation outlines some common hooks that the [`Extension`](api:SilverStripe\Core\Extension) API provides specifically for managing +data records. Note that this is *not* an exhaustive list - we encourage you to look at the source code to see what other extension hooks are available. [warning] Avoid using the hooks shown here for checking permissions or validating data - there are specific mechanisms for handling those scenarios. See [permissions](permissions) and [validation](validation) respectively. [/warning] -## onBeforeWrite +## `onBeforeWrite` You can customise saving behavior for each `DataObject`, e.g. for adding workflow or data customization. The function is triggered when calling [`write()`](api:SilverStripe\ORM\DataObject::write()) to save the object to the database. This includes saving a page in the CMS or altering @@ -23,11 +23,11 @@ a record via code. Example: Make sure the player has a valid and unique player number for their team when being assigned a new team. ```php -use SilverStripe\Control\HTTPResponse_Exception; -use SilverStripe\Security\Security; +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $db = [ // ... @@ -37,14 +37,14 @@ class Player extends DataObject private static $has_one = [ 'Team' => Team::class, ]; - - public function onBeforeWrite() + + public function onBeforeWrite() { // Use $this->isInDb() to check if the record is being written to the database for the first time if (!$this->isInDb() && $this->Team()->exists()) { $this->Number = $this->Team()->getAvailablePlayerNumber(); } - + // If the player changed teams if ($this->isChanged('TeamID') && $this->Team()->exists()) { // If the player's number is already used by someone else on this team @@ -53,7 +53,7 @@ class Player extends DataObject $this->Number = $this->Team()->getAvailablePlayerNumber(); } } - + // CAUTION: You are required to call parent::onBeforeWrite(), otherwise // SilverStripe will not execute the request. parent::onBeforeWrite(); @@ -61,18 +61,20 @@ class Player extends DataObject } ``` -## onBeforeDelete +## `onBeforeDelete` Triggered before executing [`delete()`](api:SilverStripe\ORM\DataObject::delete()) on an existing object. It can be useful if you need to make sure you clean up some other data/files/etc which aren't directly associated with the actual `DataObject` record. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { // ... - public function onBeforeDelete() + public function onBeforeDelete() { /* Do some cleanup here relevant to your project before deleting the actual database record */ @@ -84,9 +86,10 @@ class Player extends DataObject ``` [notice] -Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle +Note: There are no separate methods for `onBeforeCreate()` and `onBeforeUpdate()`. Please check `$this->isInDb()` to toggle these two modes, as shown in the example above. [/notice] -## Related Lessons -* [Working with data relationships - $has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) +## Related lessons + +- [Working with data relationships - $has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1) diff --git a/en/02_Developer_Guides/00_Model/06_SearchFilters.md b/en/02_Developer_Guides/00_Model/06_SearchFilters.md index f37769fc5..8b84255f2 100644 --- a/en/02_Developer_Guides/00_Model/06_SearchFilters.md +++ b/en/02_Developer_Guides/00_Model/06_SearchFilters.md @@ -4,19 +4,19 @@ summary: Use suffixes on your ORM queries. icon: search --- -# SearchFilter Modifiers +# `SearchFilter` modifiers The `filter()`, `exclude()`, and other related methods on [`DataList`](api:SilverStripe\ORM\DataList), [`ArrayList`](api:SilverStripe\ORM\ArrayList), and [`EagerLoadedList`](api:SilverStripe\ORM\EagerLoadedList) specify exact matches by default. However, when calling these methods, there are a number of suffixes that you can put on field names to change this behavior. These are represented as `SearchFilter` subclasses and include: - * [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter) - * [StartsWithFilter](api:SilverStripe\ORM\Filters\StartsWithFilter) - * [EndsWithFilter](api:SilverStripe\ORM\Filters\EndsWithFilter) - * [PartialMatchFilter](api:SilverStripe\ORM\Filters\PartialMatchFilter) - * [GreaterThanFilter](api:SilverStripe\ORM\Filters\GreaterThanFilter) - * [GreaterThanOrEqualFilter](api:SilverStripe\ORM\Filters\GreaterThanOrEqualFilter) - * [LessThanFilter](api:SilverStripe\ORM\Filters\LessThanFilter) - * [LessThanOrEqualFilter](api:SilverStripe\ORM\Filters\LessThanOrEqualFilter) +- [`ExactMatchFilter`](api:SilverStripe\ORM\Filters\ExactMatchFilter) +- [`StartsWithFilter`](api:SilverStripe\ORM\Filters\StartsWithFilter) +- [`EndsWithFilter`](api:SilverStripe\ORM\Filters\EndsWithFilter) +- [`PartialMatchFilter`](api:SilverStripe\ORM\Filters\PartialMatchFilter) +- [`GreaterThanFilter`](api:SilverStripe\ORM\Filters\GreaterThanFilter) +- [`GreaterThanOrEqualFilter`](api:SilverStripe\ORM\Filters\GreaterThanOrEqualFilter) +- [`LessThanFilter`](api:SilverStripe\ORM\Filters\LessThanFilter) +- [`LessThanOrEqualFilter`](api:SilverStripe\ORM\Filters\LessThanOrEqualFilter) See [`SilverStripe\ORM\Filters` in the API docs](api:SilverStripe\ORM\Filters) for a full list of `SearchFilter` classes available in silverstripe/framework. @@ -26,13 +26,13 @@ An example of a `SearchFilter` in use: // fetch any player whose first name starts with the letter 'S' and has a PlayerNumber greater than 10 $players = Player::get()->filter([ 'FirstName:StartsWith' => 'S', - 'PlayerNumber:GreaterThan' => '10' + 'PlayerNumber:GreaterThan' => '10', ]); // fetch any player whose name contains the letter 'z' $players = Player::get()->filterAny([ 'FirstName:PartialMatch' => 'z', - 'LastName:PartialMatch' => 'z' + 'LastName:PartialMatch' => 'z', ]); ``` @@ -62,38 +62,43 @@ Though note that for backwards compatibility reasons, `ArrayList` is explicitly SilverStripe\ORM\ArrayList: default_case_sensitive: false ``` + [/info] ```php // Fetch players that their FirstName is exactly 'Sam' // Caution: This might be case in-sensitive if MySQL or MSSQL is used $players = Player::get()->filter([ - 'FirstName:ExactMatch' => 'Sam' + 'FirstName:ExactMatch' => 'Sam', ]); // Fetch players that their FirstName is exactly 'Sam' (force case-sensitive) $players = Player::get()->filter([ - 'FirstName:ExactMatch:case' => 'Sam' + 'FirstName:ExactMatch:case' => 'Sam', ]); // Fetch players that their FirstName is exactly 'Sam' (force NOT case-sensitive) $players = Player::get()->filter([ - 'FirstName:ExactMatch:nocase' => 'Sam' + 'FirstName:ExactMatch:nocase' => 'Sam', ]); ``` By default the `:ExactMatch` filter is applied, so we can shorthand the above to: + ```php -$players = Player::get()->filter('FirstName', 'Sam'); // Default DB engine behaviour -$players = Player::get()->filter('FirstName:case', 'Sam'); // case-sensitive -$players = Player::get()->filter('FirstName:nocase', 'Sam'); // NOT case-sensitive +// Default DB engine behaviour +$players = Player::get()->filter('FirstName', 'Sam'); +// case-sensitive +$players = Player::get()->filter('FirstName:case', 'Sam'); +// NOT case-sensitive +$players = Player::get()->filter('FirstName:nocase', 'Sam'); ``` Note that all search filters (e.g. `:PartialMatch`) refer to services registered with [`Injector`](api:SilverStripe\Core\Injector\Injector) within the `DataListFilter.` prefixed namespace. New filters can be registered using the below yml config: -```yaml +```yml SilverStripe\Core\Injector\Injector: DataListFilter.CustomMatch: class: MyVendor\Search\CustomMatchFilter @@ -103,12 +108,12 @@ The following is a query which will return everyone whose first name starts with ```php $players = Player::get()->filter([ - 'FirstName:StartsWith:nocase' => 'S' + 'FirstName:StartsWith:nocase' => 'S', ]); // use :not to get everyone whose first name does NOT start with "S" $players = Player::get()->filter([ - 'FirstName:StartsWith:not' => 'S' + 'FirstName:StartsWith:not' => 'S', ]); ``` @@ -117,18 +122,20 @@ You can combine `:not` and either `:nocase` or `:case`. Note that the order does ```php $players = Player::get()->filter([ - 'FirstName:StartsWith:nocase:not' => 'S' + 'FirstName:StartsWith:nocase:not' => 'S', ]); $players = Player::get()->filter([ - 'FirstName:StartsWith:not:nocase' => 'S' + 'FirstName:StartsWith:not:nocase' => 'S', ]); ``` + [/hint] -## Related Lessons -* [Introduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) -* [Building a search form](https://www.silverstripe.org/learn/lessons/v4/building-a-search-form-1) +## Related lessons + +- [Introduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) +- [Building a search form](https://www.silverstripe.org/learn/lessons/v4/building-a-search-form-1) -## API Documentation +## API documentation -* [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) +- [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) diff --git a/en/02_Developer_Guides/00_Model/07_Permissions.md b/en/02_Developer_Guides/00_Model/07_Permissions.md index 6c99fb2d2..d33a7abad 100644 --- a/en/02_Developer_Guides/00_Model/07_Permissions.md +++ b/en/02_Developer_Guides/00_Model/07_Permissions.md @@ -4,9 +4,9 @@ summary: Reduce risk by securing models. icon: lock --- -# Model-Level Permissions +# Model-Level permissions -Models can be modified in a variety of controllers and user interfaces, all of which can implement their own security +Models can be modified in a variety of controllers and user interfaces, all of which can implement their own security checks. Often it makes sense to centralize those checks on the model, regardless of the used controller. The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`. @@ -15,7 +15,7 @@ The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `can Versioned models have additional permission methods - see [Version specific `can` methods](versioning#permission-methods). [/hint] -Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive +Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive a `$member` argument, and default to the currently logged in member (through `Security::getCurrentUser()`). [notice] @@ -26,43 +26,47 @@ Make sure you implement these methods for models which should be editable by mem In this example, the `MyDataObject` model can be viewed, edited, deleted, and created by any user with the `CMS_ACCESS_CMSMain` permission code, aka "Access to 'Pages' section". ```php -use SilverStripe\Security\Permission; +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\Security\Permission; -class MyDataObject extends DataObject +class MyDataObject extends DataObject { - public function canView($member = null) + public function canView($member = null) { return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } - public function canEdit($member = null) + public function canEdit($member = null) { return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } - public function canDelete($member = null) + public function canDelete($member = null) { return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } - public function canCreate($member = null, $context = []) + public function canCreate($member = null, $context = []) { return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } } ``` -It is good practice to let extensions extend permissions unless you _explicitly_ want a very restrictive permissions model. This is already done by default in the implementations of these methods in `DataObject`. +It is good practice to let extensions extend permissions unless you *explicitly* want a very restrictive permissions model. This is already done by default in the implementations of these methods in `DataObject`. You might also want to validate that the parent class doesn't deny access for a given action. ```php +namespace App\Model\MyDataObject; + use SilverStripe\Security\Permission; -class MyDataObject extends SomeParentObject +class MyDataObject extends SomeParentObject { - public function canView($member = null) + public function canView($member = null) { // If any extension returns false, the result will be false // otherwise if any extension returns true, the result will be true @@ -78,7 +82,7 @@ class MyDataObject extends SomeParentObject return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } - public function canEdit($member = null) + public function canEdit($member = null) { // If the parent class says the member can't perform this action, don't let them do it. // Be careful though - if the parent class doesn't explicitly implement canEdit(), you will end up @@ -91,7 +95,6 @@ class MyDataObject extends SomeParentObject return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); } } - ``` See the [User Permissions](/developer_guides/security/permissions/) section for more information about defining permissions. @@ -106,11 +109,13 @@ checked in the invoking code. The CMS default sections as well as custom interfa You can extend the permissions checks for and `DataObject` by implementing an [`Extension`](api:SilverStripe\Core\Extension). -It is good practice to only return `null` or `false` from these methods. Returning `false` means the user is _not_ allowed to perform the action, and `null` means the record should perform the rest of its own permission checks to validate if the user can perform the action. +It is good practice to only return `null` or `false` from these methods. Returning `false` means the user is *not* allowed to perform the action, and `null` means the record should perform the rest of its own permission checks to validate if the user can perform the action. -If you return `true` from these methods, you're saying the user _is_ allowed to perform the action, and that the model shouldn't perform any more permissions checks. +If you return `true` from these methods, you're saying the user *is* allowed to perform the action, and that the model shouldn't perform any more permissions checks. ```php +namespace App\Extension; + use SilverStripe\Core\Extension; use SilverStripe\Security\Permission; @@ -128,7 +133,7 @@ class PermissionsExtension extends Extension See [Extensions and DataExtensions](/developer_guides/extending/extensions/) for more information about extensions. -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) -* [Permission](api:SilverStripe\Security\Permission) +- [DataObject](api:SilverStripe\ORM\DataObject) +- [Permission](api:SilverStripe\Security\Permission) diff --git a/en/02_Developer_Guides/00_Model/08_SQL_Select.md b/en/02_Developer_Guides/00_Model/08_SQL_Select.md index 33bcaa412..feb2c8f89 100644 --- a/en/02_Developer_Guides/00_Model/08_SQL_Select.md +++ b/en/02_Developer_Guides/00_Model/08_SQL_Select.md @@ -4,7 +4,7 @@ summary: Write and modify direct database queries through SQLExpression subclass iconBrand: searchengin --- -# SQL Queries +# SQL queries Most of the time you will be using the ORM abstraction layer to interact with the database (see [Introduction to the Data Model and ORM](/developer_guides/model/data_model_and_orm)), @@ -42,14 +42,14 @@ $count = SQLSelect::create('COUNT(*)', $memberTable)->execute()->value(); $count = Member::get()->count(); ``` -If you do use raw SQL, you'll run the risk of breaking +If you do use raw SQL, you'll run the risk of breaking various assumptions the ORM and code based on it have: -* Custom getters/setters (object property values can differ from database column values) -* DataObject hooks like `onBeforeWrite()` and `onBeforeDelete()` if running low-level `INSERT` or `UPDATE` queries -* Automatic casting -* Default values set through objects -* Database abstraction (some `DataObject` classes may not have their own tables, or may need a `JOIN` with other tables to get all of their field values) +- Custom getter/setter methods (object property values can differ from database column values) +- `DataObject` hooks like `onBeforeWrite()` and `onBeforeDelete()` if running low-level `INSERT` or `UPDATE` queries +- Automatic casting +- Default values set through objects +- Database abstraction (some `DataObject` classes may not have their own tables, or may need a `JOIN` with other tables to get all of their field values) We'll explain some ways to use the low-level APIs with the full power of SQL, but still maintain a connection to the ORM where possible. @@ -65,11 +65,11 @@ how to properly prepare user input and variables for use in queries While you could hardcode table names into your SQL queries, that invites human error and means you have to make sure you know exactly what table stores which data for every class in the class hierarchy of the model you're interested in. Luckily, the [`DataObjectSchema`](api:SilverStripe\ORM\DataObjectSchema) class knows all about the database schema for your `DataObject` models. The following methods in particular may be useful to you: -* [`baseDataTable()`](api:SilverStripe\ORM\DataObjectSchema::baseDataTable()): Get the name of the database table which holds the base data (i.e. `ID`, `ClassName`, `Created`, etc) for a given `DataObject` class -* [`classHasTable()`](api:SilverStripe\ORM\DataObjectSchema::classHasTable()): Check if there is a table in the database for a given `DataObject` class (i.e. whether that class defines columns not already present in another class further up the class hierarchy) -* [`sqlColumnForField()`](api:SilverStripe\ORM\DataObjectSchema::sqlColumnForField()): Get the ANSI-quoted table and column name for a given `DataObject` field (in `"Table"."Field"` format) -* [`tableForField()`](api:SilverStripe\ORM\DataObjectSchema::tableForField()): Get the table name in the class hierarchy which contains a given field column. -* [`tableName()`](api:SilverStripe\ORM\DataObjectSchema::tableName()): Get table name for the given class. Note that this does not confirm a table actually exists (or should exist), but returns the name that would be used if this table did exist. Male sure to call `classHasTable()` before using this table name in a query. +- [`baseDataTable()`](api:SilverStripe\ORM\DataObjectSchema::baseDataTable()): Get the name of the database table which holds the base data (i.e. `ID`, `ClassName`, `Created`, etc) for a given `DataObject` class +- [`classHasTable()`](api:SilverStripe\ORM\DataObjectSchema::classHasTable()): Check if there is a table in the database for a given `DataObject` class (i.e. whether that class defines columns not already present in another class further up the class hierarchy) +- [`sqlColumnForField()`](api:SilverStripe\ORM\DataObjectSchema::sqlColumnForField()): Get the ANSI-quoted table and column name for a given `DataObject` field (in `"Table"."Field"` format) +- [`tableForField()`](api:SilverStripe\ORM\DataObjectSchema::tableForField()): Get the table name in the class hierarchy which contains a given field column. +- [`tableName()`](api:SilverStripe\ORM\DataObjectSchema::tableName()): Get table name for the given class. Note that this does not confirm a table actually exists (or should exist), but returns the name that would be used if this table did exist. Male sure to call `classHasTable()` before using this table name in a query. [hint] While the default database connector will work fine without explicitly ANSI-quoting table names in queries, it is good practice to make sure they are quoted (especially if you're writing these queries in a module that will be publicly shared) to ensure your queries will work on other database connectors such as [`PostgreSQLDatabase`](https://github.com/silverstripe/silverstripe-postgresql) which explicitly require ANSI quoted table names. @@ -87,7 +87,7 @@ Selection can be done by creating an instance of [`SQLSelect`](api:SilverStripe\ management of all elements of a SQL `SELECT` query, including columns, joined tables, conditional filters, grouping, limiting, and sorting. -E.g: +For example: ```php $schema = DataObject::getSchema(); @@ -102,7 +102,9 @@ $sqlQuery->selectField('FieldName'); $sqlQuery->selectField('YEAR("Birthday")', 'Birthyear'); // Join another table onto the query -$joinOnClause = $schema->sqlColumnForField(Player::class, 'TeamID') . ' = ' . $schema->sqlColumnForField(Team::class, 'ID'); +$teamIdField = $schema->sqlColumnForField(Player::class, 'TeamID'); +$idField = $schema->sqlColumnForField(Team::class, 'ID'); +$joinOnClause = "$teamIdField = $idField"; $sqlQuery->addLeftJoin($teamTableName, $joinOnClause); // Combine another query using a union @@ -123,7 +125,7 @@ $rawSQL = $sqlQuery->sql($parameters); $result = $sqlQuery->execute(); // Iterate over results -foreach($result as $row) { +foreach ($result as $row) { echo $row['BirthYear']; } ``` @@ -144,8 +146,8 @@ For example, creating a `SQLDelete` object: ```php use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLDelete; $schema = DataObject::getSchema(); @@ -187,40 +189,40 @@ Each of these classes implement the [`SQLWriteExpression`](api:SilverStripe\ORM\ accepts key/value pairs in a number of similar ways. These include the following API methods: - * [`addAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::addAssignments()) - Takes a list of assignments as an associative array of key => value pairs, +- [`addAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::addAssignments()) - Takes a list of assignments as an associative array of key => value pairs, where the value can also be an SQL expression. - * [`setAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::setAssignments()) - Replaces all existing assignments with the specified list - * [`getAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::getAssignments()) - Returns all currently given assignments, as an associative array +- [`setAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::setAssignments()) - Replaces all existing assignments with the specified list +- [`getAssignments()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::getAssignments()) - Returns all currently given assignments, as an associative array in the format `['Column' => ['SQL' => ['parameters]]]` - * [`assign()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::assign()) - Singular form of `addAssignments()`, but only assigns a single column value - * [`assignSQL()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::assignSQL()) - Assigns a column the value of a specified SQL expression without parameters - +- [`assign()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::assign()) - Singular form of `addAssignments()`, but only assigns a single column value +- [`assignSQL()`](api:SilverStripe\ORM\Queries\SQLWriteExpression::assignSQL()) - Assigns a column the value of a specified SQL expression without parameters - `assignSQL('Column', 'SQL')` is shorthand for `assign('Column', ['SQL' => []])` `SQLUpdate` also includes the following API methods: - * [`clear()`](api:SilverStripe\ORM\Queries\SQLUpdate::clear()) - Clears all assignments - * [`getTable()`](api:SilverStripe\ORM\Queries\SQLUpdate::getTable()) - Gets the table to update - * [`setTable()`](api:SilverStripe\ORM\Queries\SQLUpdate::setTable()) - Sets the table to update (this should be ANSI-quoted) +- [`clear()`](api:SilverStripe\ORM\Queries\SQLUpdate::clear()) - Clears all assignments +- [`getTable()`](api:SilverStripe\ORM\Queries\SQLUpdate::getTable()) - Gets the table to update +- [`setTable()`](api:SilverStripe\ORM\Queries\SQLUpdate::setTable()) - Sets the table to update (this should be ANSI-quoted) e.g. `$query->setTable('"Page"');` `SQLInsert` also includes the following API methods: - * [`clear()`](api:SilverStripe\ORM\Queries\SQLInsert::clear()) - Clears all rows - * [`clearRow()`](api:SilverStripe\ORM\Queries\SQLInsert::clearRow()) - Clears all assignments on the current row - * [`addRow()`](api:SilverStripe\ORM\Queries\SQLInsert::addRow()) - Adds another row of assignments, and sets the current row to the new row - * [`addRows()`](api:SilverStripe\ORM\Queries\SQLInsert::addRows()) - Adds a number of arrays, each representing a list of assignment rows, +- [`clear()`](api:SilverStripe\ORM\Queries\SQLInsert::clear()) - Clears all rows +- [`clearRow()`](api:SilverStripe\ORM\Queries\SQLInsert::clearRow()) - Clears all assignments on the current row +- [`addRow()`](api:SilverStripe\ORM\Queries\SQLInsert::addRow()) - Adds another row of assignments, and sets the current row to the new row +- [`addRows()`](api:SilverStripe\ORM\Queries\SQLInsert::addRows()) - Adds a number of arrays, each representing a list of assignment rows, and sets the current row to the last one - * [`getColumns()`](api:SilverStripe\ORM\Queries\SQLInsert::getColumns()) - Gets the names of all distinct columns assigned - * [`getInto()`](api:SilverStripe\ORM\Queries\SQLInsert::getInto()) - Gets the table to insert into - * [`setInto()`](api:SilverStripe\ORM\Queries\SQLInsert::setInto()) - Sets the table to insert into (this should be ANSI-quoted), +- [`getColumns()`](api:SilverStripe\ORM\Queries\SQLInsert::getColumns()) - Gets the names of all distinct columns assigned +- [`getInto()`](api:SilverStripe\ORM\Queries\SQLInsert::getInto()) - Gets the table to insert into +- [`setInto()`](api:SilverStripe\ORM\Queries\SQLInsert::setInto()) - Sets the table to insert into (this should be ANSI-quoted), e.g. `$query->setInto('"Page"');` -E.g.: +For example: ```php use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLUpdate; $schema = DataObject::getSchema(); @@ -231,7 +233,7 @@ $update = SQLUpdate::create($siteTreeTable)->addWhere(['"ID"' => 3]); // assigning a list of items $update->addAssignments([ '"Title"' => 'Our Products', - '"MenuTitle"' => 'Products' + '"MenuTitle"' => 'Products', ]); // Assigning a single value @@ -241,7 +243,7 @@ $update->assign('"MenuTitle"', 'Products'); $title = 'Products'; $update->assign('"MenuTitle"', [ 'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' => - [$title, $title] + [$title, $title], ]); // Assigning a value using a pure SQL expression @@ -259,8 +261,8 @@ For example: ```php use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLInsert; $schema = DataObject::getSchema(); @@ -271,7 +273,7 @@ $insert = SQLInsert::create($siteTreeTable); // Add multiple rows in a single call. Note that column names do not need to be symmetric $insert->addRows([ ['"Title"' => 'Home', '"Content"' => '

This is our home page

'], - ['"Title"' => 'About Us', '"ClassName"' => 'AboutPage'] + ['"Title"' => 'About Us', '"ClassName"' => 'AboutPage'], ]); // Adjust an assignment on the last row @@ -286,16 +288,15 @@ $columns = $insert->getColumns(); $insert->execute(); ``` -### Value Checks +### Value checks -Raw SQL is handy for performance-optimized calls, -e.g. when you want a single column rather than a full-blown object representation. +Raw SQL is handy for performance-optimized calls, e.g. when you want a single column rather than a full-blown object representation. Example: Get the count from a relationship. ```php -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLSelect; $schema = DataObject::getSchema(); @@ -336,8 +337,8 @@ Also note that you can pass an integer in as the first argument rather than an a Example: Get the fields for all players in a team which has more than 15 wins. ```php -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLSelect; $schema = DataObject::getSchema(); @@ -348,7 +349,7 @@ $placeholders = DB::placeholders($teamIds); $sqlQuery = new SQLSelect(); $sqlQuery->setFrom($playerTableName)->where([ - $schema->sqlColumnForField(Player::class, 'ID') . ' in (' . $placeholders . ')' => $ids + $schema->sqlColumnForField(Player::class, 'ID') . ' in (' . $placeholders . ')' => $ids, ]); $results = $sqlQuery->execute(); ``` @@ -359,17 +360,18 @@ This is obviously a contrived example - this could easily (and more efficiently) ```php $players = Player::get()->filter('Teams.Wins:GreaterThan', 15); ``` + [/info] -### Joining tables for a DataObject inheritance chain {#joins-for-inheritance} +### Joining tables for a `DataObject` inheritance chain {#joins-for-inheritance} In the [Introduction to the Data Model and ORM](data_model_and_orm/#subclasses) we discussed how `DataObject` inheretance chains can spread their data across multiple tables. The ORM handles this seemlessly, but when using the lower-level APIs we need to account for this ourselves by joining all of the relevant tables manually. -We also want to make sure to _only_ select the records which are relevant for the actual class in the class hierarchy we're looking at. To do that, we can either use an `INNER JOIN`, or we can use a `WHERE` clause on the `ClassName` field. In the below example we're using a `WHERE` clause with a `LEFT JOIN` because it is likely more intuitive for developers who aren't intimately familar with SQL. +We also want to make sure to *only* select the records which are relevant for the actual class in the class hierarchy we're looking at. To do that, we can either use an `INNER JOIN`, or we can use a `WHERE` clause on the `ClassName` field. In the below example we're using a `WHERE` clause with a `LEFT JOIN` because it is likely more intuitive for developers who aren't intimately familar with SQL. ```php -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLSelect; $schema = DataObject::getSchema(); @@ -400,7 +402,7 @@ foreach ($columns as $alias => $ansiQuotedColumn) { ``` [hint] -If we want all of the fields for _all_ models in the class hierarchy (mimicking `Product::get()` where `Product` is the first subclass of `DataObject` - see the example in the [Introduction to the Data Model and ORM](data_model_and_orm/#subclasses)), we can do this by using a `LEFT JOIN` (like above), ommitting the `WHERE` clause on the `ClassName` field, and making sure we join _all_ tables for the inheritance chain regardless of the fields being selected. To do that, make sure you're using the first `DataObject` class as your first main query class (replace `Computer` above with `Product`, in this example), remove the call to `$select->addWhere()`, and add the following code to the end of the above example: +If we want all of the fields for *all* models in the class hierarchy (mimicking `Product::get()` where `Product` is the first subclass of `DataObject` - see the example in the [Introduction to the Data Model and ORM](data_model_and_orm/#subclasses)), we can do this by using a `LEFT JOIN` (like above), ommitting the `WHERE` clause on the `ClassName` field, and making sure we join *all* tables for the inheritance chain regardless of the fields being selected. To do that, make sure you're using the first `DataObject` class as your first main query class (replace `Computer` above with `Product`, in this example), remove the call to `$select->addWhere()`, and add the following code to the end of the above example: ```php // Make sure we join all the tables for the model inheritance chain @@ -415,9 +417,10 @@ foreach (ClassInfo::subclassesFor(Product::class, includeBaseClass: false) as $c } } ``` + [/hint] -### Common Table Expressions (CTEs aka the `WITH` clause) {#cte} +### Common table expressions (CTE aka the `WITH` clause) {#cte} Common Table Expressions are a powerful tool both for optimising complex queries, and for creating recursive queries. You can use these by calling the [`SQLSelect::addWith()`](api:SilverStripe\ORM\Queries\SQLSelect::addWith()) method. @@ -438,8 +441,8 @@ For an example of how to use this abstraction and how powerful it is, here is an ```php use App\Model\ObjectWithParent; use SilverStripe\Core\Convert; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLSelect; $schema = DataObject::getSchema(); @@ -468,7 +471,8 @@ if (DB::get_conn()->supportsCteQueries(true)) { ] ); $cteQuery->addUnion($recursiveQuery); - $baseQuery->addWith('hierarchy_cte', $cteQuery, ['parent_id'], true)->addInnerJoin('hierarchy_cte', "$idField = $cteIdField"); + $baseQuery->addWith('hierarchy_cte', $cteQuery, ['parent_id'], true) + ->addInnerJoin('hierarchy_cte', "$idField = $cteIdField"); // This query result will include only the ancestors of whatever record is stored in the $someRecord variable. $ancestors = $baseQuery->execute(); } else { @@ -488,22 +492,22 @@ WITH RECURSIVE "hierarchy_cte" ("parent_id") AS ( WHERE ("ObjectWithParent"."ParentID" > 0) AND ("ObjectWithParent"."ID" = "hierarchy_cte"."parent_id") ) ) -SELECT * FROM "ObjectWithParent" INNER JOIN "hierarchy_cte" ON "ObjectWithParent"."ID" = "hierarchy_cte"."parent_id" +SELECT * FROM "ObjectWithParent" INNER JOIN "hierarchy_cte" ON "ObjectWithParent"."ID" = "hierarchy_cte"."parent_id" ``` The PHPDoc for the [`SQLSelect::addWith()`](api:SilverStripe\ORM\Queries\SQLSelect::addWith()) method has more details about what each of the arguments are and how they're used, though note that you should ensure you understand the underlying SQL concept of CTE queries before using this API. ### Mapping -Creates a map based on the first two columns of the query result. +Creates a map based on the first two columns of the query result. This can be useful for creating dropdowns. Example: Show player names with their birth year, but set their birth dates as values. ```php use SilverStripe\Forms\DropdownField; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Queries\SQLSelect; $schema = DataObject::getSchema(); @@ -515,7 +519,8 @@ $sqlQuery->setSelect('"ID"'); $sqlQuery->selectField('CONCAT("Name", \' - \', YEAR("Birthdate")', 'NameWithBirthyear'); $map = $sqlQuery->execute()->map(); -// The value of the selected option will be the record ID, and the display label will be the name and birthyear concatenation. +// The value of the selected option will be the record ID, and the display label will be the name and +// birthyear concatenation. $field = new DropdownField('Birthdates', 'Birthdates', $map); ``` @@ -524,13 +529,15 @@ because of the custom SQL value transformation (`YEAR()`). An alternative approach would be a custom getter in the object definition: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $db = [ - 'Name' => 'Varchar', - 'Birthdate' => 'Date' + 'Name' => 'Varchar', + 'Birthdate' => 'Date', ]; public function getNameWithBirthyear() @@ -538,8 +545,13 @@ class Player extends DataObject return date('y', $this->Birthdate); } } +``` + +```php +use App\Model\Player; -$map = Player::get()->map('ID', 'NameWithBirthyear'); +$players = Player::get(); +$map = $players->map('ID', 'NameWithBirthyear'); ``` ### True raw SQL @@ -550,8 +562,8 @@ Directly querying the database: ```php use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\DataObject; $schema = DataObject::getSchema(); $siteTreeBaseTable = DB::get_conn()->escapeIdentifier($schema->baseDataTable(SiteTree::class)); @@ -568,31 +580,31 @@ foreach ($results as $row) { ``` [hint] -Note that you do _not_ have to call `execute()` with these methods, unlike the abstraction layer in the other examples. This is because you're passing the entire query into the method - you can't change the query after it's passed in, so it gets executed right away. The return type for these methods is the same as the return type for the [`execute()`](api::SilverStripe\ORM\Queries\SQLExpression::execute()) methods on the `SQLExpression` classes. +Note that you do *not* have to call `execute()` with these methods, unlike the abstraction layer in the other examples. This is because you're passing the entire query into the method - you can't change the query after it's passed in, so it gets executed right away. The return type for these methods is the same as the return type for the [`execute()`](api::SilverStripe\ORM\Queries\SQLExpression::execute()) methods on the `SQLExpression` classes. [/hint] ### Data types The following PHP types are used to return database content: - * booleans will be an integer 1 or 0, to ensure consistency with MySQL that doesn't have native booleans - * integer types returned as integers - * floating point / decimal types returned as floats - * strings returned as strings - * dates / datetimes returned as strings +- booleans will be an integer 1 or 0, to ensure consistency with MySQL that doesn't have native booleans +- integer types returned as integers +- floating point / decimal types returned as floats +- strings returned as strings +- dates / datetimes returned as strings -## Related Lessons -* [Building custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1) +## Related lessons +- [Building custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1) -## Related Documentation +## Related documentation -* [Introduction to the Data Model and ORM](data_model_and_orm) +- [Introduction to the Data Model and ORM](data_model_and_orm) -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) -* [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) -* [DB](api:SilverStripe\ORM\DB) -* [Query](api:SilverStripe\ORM\Connect\Query) -* [Database](api:SilverStripe\ORM\Connect\Database) +- [DataObject](api:SilverStripe\ORM\DataObject) +- [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) +- [DB](api:SilverStripe\ORM\DB) +- [Query](api:SilverStripe\ORM\Connect\Query) +- [Database](api:SilverStripe\ORM\Connect\Database) diff --git a/en/02_Developer_Guides/00_Model/09_Validation.md b/en/02_Developer_Guides/00_Model/09_Validation.md index c56a49b1b..1f16aeda7 100644 --- a/en/02_Developer_Guides/00_Model/09_Validation.md +++ b/en/02_Developer_Guides/00_Model/09_Validation.md @@ -4,14 +4,14 @@ summary: Validate your data at the model level icon: check-square --- -# Validation and Constraints +# Validation and constraints Traditionally, validation in Silverstripe CMS has been mostly handled through [form validation](../forms). While this is a useful approach, it can lead to data inconsistencies if the record is modified outside of the form context. Most validation constraints are actually data constraints which belong on the model. Silverstripe CMS provides the -[DataObject::validate()](api:SilverStripe\ORM\DataObject::validate()) method for this purpose. The `validate()` method is +[`DataObject::validate()`](api:SilverStripe\ORM\DataObject::validate()) method for this purpose. The `validate()` method is called any time the `write()` method is called, before the `onBeforeWrite()` extension hook. By default, there is no validation - objects are always valid! However, you can override this method in your `DataObject` @@ -25,21 +25,22 @@ write, and respond appropriately if it isn't. The return value of `validate()` is a [`ValidationResult`](api:SilverStripe\ORM\ValidationResult) object. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyObject extends DataObject +class MyObject extends DataObject { - private static $db = [ 'Country' => 'Varchar', - 'Postcode' => 'Varchar' + 'Postcode' => 'Varchar', ]; - public function validate() + public function validate() { $result = parent::validate(); - if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) { + if ($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) { $result->addError('Need five digits for German postcodes'); } @@ -48,7 +49,7 @@ class MyObject extends DataObject } ``` -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) -* [ValidationResult](api:SilverStripe\ORM\ValidationResult); +- [DataObject](api:SilverStripe\ORM\DataObject) +- [ValidationResult](api:SilverStripe\ORM\ValidationResult); diff --git a/en/02_Developer_Guides/00_Model/10_Versioning.md b/en/02_Developer_Guides/00_Model/10_Versioning.md index 32726c4e8..e1ce7292c 100644 --- a/en/02_Developer_Guides/00_Model/10_Versioning.md +++ b/en/02_Developer_Guides/00_Model/10_Versioning.md @@ -17,9 +17,10 @@ from published content shown to your website visitors. There are two complementary modules that improve content editor experience around "owned" nested objects (e.g. elemental blocks). Those are in experimental status right now, but we would appreciate any feedback and contributions. -You can check them out on github: - - https://github.com/silverstripe/silverstripe-versioned-snapshots - - https://github.com/silverstripe/silverstripe-versioned-snapshot-admin +You can check them out on GitHub: + +- +- The first one adds extra metadata to versions about object parents at the moment of version creation. The second module extends CMS History UI adding control over nested objects. @@ -34,11 +35,12 @@ This section discusses how Silverstripe CMS implements versioning and related hi ### Stages -In most cases, you'll want to have one polished version of a `Page` visible to the general public while your editors might be making additional changes on a draft version. Silverstripe CMS handles this through the concept of _stages_. +In most cases, you'll want to have one polished version of a `Page` visible to the general public while your editors might be making additional changes on a draft version. Silverstripe CMS handles this through the concept of *stages*. By default, adding the `Versioned` extension to a `DataObject` will create 2 stages: -* "Stage" for tracking draft content (aka "draft") -* "Live" for tracking content publicly visible (aka "published"). + +- "Stage" for tracking draft content (aka "draft") +- "Live" for tracking content publicly visible (aka "published"). [info] Yes, the draft stage is called the "Stage" stage. In this documentation we'll try to differentiate between the stage named "Stage" and the concept of a stage by giving the named stage a capital S and putting quotes around it - but in some cases we'll just refer to it as "draft" because often that's the more intuitive way to think of it. @@ -52,7 +54,7 @@ If you just want to keep track of the version history of a model's records but y The `Versioned` class has a `Versioned::DRAFT` constant to refer to the "Stage" stage, and `Versioned::LIVE` to refer to the "Live" stage. It can be useful to use those in your PHP code when you need to refer to the stages. [/hint] -### Ownership and relations between DataObjects {#ownership} +### Ownership and relations between `DataObject` models {#ownership} Typically when publishing versioned DataObjects, it is necessary to ensure that some linked components are published along with it. Unless this is done, site content can appear incorrectly published. @@ -67,7 +69,7 @@ It relies on a pre-existing relationship to function. If an object "owns" other objects, you'll usually want to publish the child objects when the parent object gets published. If those child objects themselves own other objects, you'll want the grand-children to be published along with the parent. -Silverstripe CMS makes this possible by using the concept of _cascade publishing_. You can choose to recursively publish an object. When an object is recursively published – either through a user action or through code – all other records it owns that implement the `Versioned` extension will automatically be published. Publication will also cascade to children of children and so on. +Silverstripe CMS makes this possible by using the concept of *cascade publishing*. You can choose to recursively publish an object. When an object is recursively published – either through a user action or through code – all other records it owns that implement the `Versioned` extension will automatically be published. Publication will also cascade to children of children and so on. A non-recursive publish operation is also available if you want to publish a new version of a object without cascade publishing all its children. @@ -83,7 +85,7 @@ An unversioned object can own a versioned object. An unversioned object can be configured to automatically publish owned versioned objects on save. -An unversioned object can also be owned by a versioned object. This can be used to recursively publish _children-of-children_ objects without requiring the intermediate relationship to go through a versioned object. This behavior can be helpful if you wish to group multiple versioned objects together. +An unversioned object can also be owned by a versioned object. This can be used to recursively publish *children-of-children* objects without requiring the intermediate relationship to go through a versioned object. This behavior can be helpful if you wish to group multiple versioned objects together. #### Ownership through media insertion in content @@ -91,7 +93,7 @@ Images and other files are tracked as versioned objects. If a file is referenced This behavior works both for versioned and unversioned objects. -### Grouping versioned DataObjects into a ChangeSet (aka Campaigns) +### Grouping versioned `DataObject` records into a `ChangeSet` (aka campaigns) Sometimes, multiple pages or records may be related in organic ways that cannot be properly expressed through an ownership relation. There's still value in being able to publish those as a block. @@ -118,19 +120,19 @@ An *explicit* inclusion is much more direct, occurring only when a user has opte It is possible for an item to be included both implicitly and explicitly in a changeset. For instance, if a page owns a file, and the page gets added to a changeset, the file is implicitly added. That same file, however, can still be added to the changeset explicitly through the file editor. In this case, the file is considered to be *explicitly* added. If the file is later removed from the changeset, it is then considered *implicitly* added, due to its owner page still being in the changeset. -## Implementing a versioned DataObject +## Implementing a versioned `DataObject` This section explains how to take a regular `DataObject` and add versioning to it. -### Applying the Versioned extension to your DataObject +### Applying the `Versioned` extension to your `DataObject` -Adding versioning to a `DataObject` model is as easy as applying the [`Versioned`](api:SilverStripe\Versioned\Versioned) extension to it, either via PHP or YAML configuration. This will apply versioning _with stages_, meaning you can have a draft and a published version of your records. +Adding versioning to a `DataObject` model is as easy as applying the [`Versioned`](api:SilverStripe\Versioned\Versioned) extension to it, either via PHP or YAML configuration. This will apply versioning *with stages*, meaning you can have a draft and a published version of your records. ```php namespace App\Model; -use SilverStripe\Versioned\Versioned; use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; class MyStagedModel extends DataObject { @@ -140,7 +142,7 @@ class MyStagedModel extends DataObject } ``` -```yaml +```yml App\Model\MyStagedModel: extensions: - SilverStripe\Versioned\Versioned @@ -151,6 +153,8 @@ can be specified by using the `.versioned` service variant that provides only ve staging. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; @@ -162,7 +166,7 @@ class VersionedModel extends DataObject } ``` -```yaml +```yml App\Model\MyStagedModel: extensions: - SilverStripe\Versioned\Versioned.versioned @@ -178,11 +182,13 @@ Versioning only works if you are adding the extension to the base class. That is of `DataObject`. Adding this extension to children of the base class will have unpredictable behaviour. [/warning] -#### Versioning a many_many relation +#### Versioning a `many_many` relation If you want to track versions of `many_many` relationships, you can do so using the ["through" setting](/developer_guides/model/relations/#many-many-through) on a `many_many` definition. This setting allows you to specify a custom `DataObject` through which to map the `many_many` relation. As such, it is possible to version your `many_many` data by versioning a "through" `DataObject`. For example: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Product extends DataObject @@ -203,6 +209,8 @@ class Product extends DataObject ``` ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; @@ -214,7 +222,7 @@ class ProductCategory extends DataObject private static $has_one = [ 'Product' => Product::class, - 'Category'=> Category::class, + 'Category' => Category::class, ]; private static $extensions = [ @@ -223,7 +231,7 @@ class ProductCategory extends DataObject } ``` -### Controlling permissions to versioned DataObjects {#permissions} +### Controlling permissions to versioned `DataObject` models {#permissions} By default, `Versioned` will come out of the box with security extensions which restrict the visibility of objects in Draft ("Stage") or Archive viewing mode. @@ -234,20 +242,20 @@ done via user code. This can be achieved either by wrapping `<% if $canView %>;` your template, or by implementing your visibility check in PHP. [/alert] -#### Version specific _can_ methods {#permission-methods} +#### Version specific *can* methods {#permission-methods} Versioned DataObjects get additional permission check methods to verify what operation a `Member` is allowed to perform: -* [`canPublish()`](api:SilverStripe\Versioned\Versioned::canPublish()): Determines if a given `Member` is allowed to publish the record -* [`canUnpublish()`](api:SilverStripe\Versioned\Versioned::canUnpublish()) Determines if a given `Member` is allowed to unpublish the record -* [`canArchive()`](api:SilverStripe\Versioned\Versioned::canArchive()) Determines if a given `Member` is allowed to archive the record -* [`canViewStage()`](api:SilverStripe\Versioned\Versioned::canViewStage()) Determines if a given `Member` can view the latest version of this record on a specific stage. Beware that this is _not_ invoked when calling `canView()`. If you want to affect the result of regular `canView()` checks, implement `canViewVersioned()` instead. -* [`canViewVersioned()`](api:SilverStripe\Versioned\Versioned::canViewVersioned()) Provides additional can view checks for versioned records. This is called by `canView()` and should not be called directly. +- [`canPublish()`](api:SilverStripe\Versioned\Versioned::canPublish()): Determines if a given `Member` is allowed to publish the record +- [`canUnpublish()`](api:SilverStripe\Versioned\Versioned::canUnpublish()) Determines if a given `Member` is allowed to unpublish the record +- [`canArchive()`](api:SilverStripe\Versioned\Versioned::canArchive()) Determines if a given `Member` is allowed to archive the record +- [`canViewStage()`](api:SilverStripe\Versioned\Versioned::canViewStage()) Determines if a given `Member` can view the latest version of this record on a specific stage. Beware that this is *not* invoked when calling `canView()`. If you want to affect the result of regular `canView()` checks, implement `canViewVersioned()` instead. +- [`canViewVersioned()`](api:SilverStripe\Versioned\Versioned::canViewVersioned()) Provides additional can view checks for versioned records. This is called by `canView()` and should not be called directly. These methods accept an optional `Member` argument. If not provided, they will assume you want to check the permission against the current `Member`. When performing a version operation on behalf of a `Member`, you'll probably want to use these methods to confirm they are authorised. [warning] -Like with the base `can` permission checks, these checks are _not_ performed automatically when invoking the associated action via PHP. i.e. if you call `publishSingle()` on a record in your own code, Silverstripe CMS will _not_ check if the currently authenticated user has permission to publish the record. Make sure you are performing permission checks by calling these `can` methods before invoking the associated actions. +Like with the base `can` permission checks, these checks are *not* performed automatically when invoking the associated action via PHP. i.e. if you call `publishSingle()` on a record in your own code, Silverstripe CMS will *not* check if the currently authenticated user has permission to publish the record. Make sure you are performing permission checks by calling these `can` methods before invoking the associated actions. [/warning] ```php @@ -275,20 +283,22 @@ $record->canViewStage(); For the `can` methods that all `DataObject` models have, see [Model-Level Permissions](permissions). -#### Customising permissions for a versioned DataObject +#### Customising permissions for a versioned `DataObject` `Versioned` record visibility can be customised in one of the following ways by editing your code: - * Override the `canViewVersioned()` method in your `DataObject` subclass. Make sure that this returns `true`, or +- Override the `canViewVersioned()` method in your `DataObject` subclass. Make sure that this returns `true`, or `false` if the user is not allowed to view this object in the current viewing mode. - * Override the `canView()` method to override the method visibility completely, regardless of what stage is being viewed. +- Override the `canView()` method to override the method visibility completely, regardless of what stage is being viewed. E.g. ```php -use SilverStripe\Versioned\Versioned; -use SilverStripe\Security\Permission; +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\Security\Permission; +use SilverStripe\Versioned\Versioned; class MyObject extends DataObject { @@ -314,8 +324,8 @@ class MyObject extends DataObject If you want to control permissions of an object in an extension, you can also implement one of the below extension hook methods in your `Extension` subclass: - * `canView()` to update the record's `canView` permissions - * `canViewNonLive()` to update the visibility of this object only in non-live mode specifically. +- `canView()` to update the record's `canView` permissions +- `canViewNonLive()` to update the visibility of this object only in non-live mode specifically. Note that unlike `canViewVersioned()`, the `canViewNonLive()` method will only be invoked if the object is in a non-published state. @@ -323,8 +333,10 @@ only be invoked if the object is in a non-published state. E.g. ```php -use SilverStripe\Security\Permission; +namespace App\Extension; + use SilverStripe\ORM\DataExtension; +use SilverStripe\Security\Permission; class MyObjectExtension extends DataExtension { @@ -346,8 +358,10 @@ permissions in the `non_live_permissions` configuration on the target model clas E.g. ```php -use SilverStripe\Versioned\Versioned; +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; class MyObject extends DataObject { @@ -364,33 +378,42 @@ class MyObject extends DataObject Versioned applies no additional permissions to `canEdit` or `canCreate`, and such these permissions should be implemented as per standard unversioned DataObjects. -### Defining ownership between related versioned DataObjects +### Defining ownership between related versioned `DataObject` models -You can use the `owns` configuration property on a `DataObject` to specify which relationships are ownership relationships. The `owns` property should be defined on the _owner_ `DataObject`. +You can use the `owns` configuration property on a `DataObject` to specify which relationships are ownership relationships. The `owns` property should be defined on the *owner* `DataObject`. For example, let's say you have a `MyPage` page type that displays banners containing an image. Each `MyPage` owns many `Banners`, which in turn owns an `Image`. ```php -use SilverStripe\Versioned\Versioned; -use SilverStripe\Assets\Image; -use SilverStripe\ORM\DataObject; +namespace App\PageType; + +use App\Model\Banner; use Page; class MyPage extends Page { private static $has_many = [ - 'Banners' => Banner::class + 'Banners' => Banner::class, ]; private static $owns = [ - 'Banners' + 'Banners', ]; } +``` + +```php +namespace App\Model; + +use App\PageType\MyPage; +use SilverStripe\Assets\Image; +use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; class Banner extends DataObject { private static $extensions = [ - Versioned::class + Versioned::class, ]; private static $has_one = [ @@ -399,7 +422,7 @@ class Banner extends DataObject ]; private static $owns = [ - 'Image' + 'Image', ]; } ``` @@ -409,14 +432,16 @@ If a `MyPage` record gets published, all its related `Banners` will also be publ Note that this relationship is for publishing specifically, and is not affected by unpublishing or archiving the owner record. To ensure unpublish and archive actions affect owned records, `cascade_deletes` must be used. See [Cascading deletions](relations/#cascading-deletions) for more information about this interaction. ```php +namespace App\PageType; + class MyPage extends Page { private static $has_many = [ - 'Banners' => Banner::class + 'Banners' => Banner::class, ]; private static $cascade_deletes = [ - 'Banners' + 'Banners', ]; } ``` @@ -427,7 +452,7 @@ You must declare both `owns` and `cascade_deletes` if you want all publish, unpu Note that ownership cannot be used with polymorphic relations (i.e. `has_one` to non-type specific `DataObject`). [/info] -#### Unversioned DataObject ownership +#### Unversioned `DataObject` ownership Ownership can be used with non-versioned DataObjects, as the necessary functionality is included by default by the versioned object through the [`RecursivePublishable`](api:SilverStripe\Versioned\RecursivePublishable) extension which is @@ -439,7 +464,7 @@ However, it is important to note that even when saving un-versioned objects, it The `owns` feature works the same regardless of whether these objects are versioned, so you can use any combination of versioned or unversioned dataobjects. You only need to call `publishRecursive()` on the specific record for which you are saving changes. -#### DataObject ownership with custom relations +#### `DataObject` ownership with custom relations In some cases you might need to apply ownership where there is no underlying database relation, such as those calculated at runtime based on business logic. In cases where you are not backing ownership @@ -450,20 +475,22 @@ This can be done by creating methods on both sides of your relation (e.g. parent that can be used to traverse between each, and then by ensuring you configure both `owns` config (on the parent) and `owned_by` (on the child). -E.g. +For example: ```php -use SilverStripe\Versioned\Versioned; +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; class MyParent extends DataObject { private static $extensions = [ - Versioned::class + Versioned::class, ]; private static $owns = [ - 'ChildObjects' + 'ChildObjects', ]; public function ChildObjects() @@ -471,15 +498,22 @@ class MyParent extends DataObject return MyChild::get(); } } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; +use SilverStripe\Versioned\Versioned; class MyChild extends DataObject { private static $extensions = [ - Versioned::class + Versioned::class, ]; private static $owned_by = [ - 'Parent' + 'Parent', ]; public function Parent() @@ -500,7 +534,7 @@ The ownership relationship is tracked through an `[image]` [shortcode](/develope which is automatically transformed into an `` tag at render time. In addition to storing the image path, the shortcode references the database identifier of the `Image` object and ensures it's published appropriately. -### Controlling how CMS users interact with versioned DataObjects +### Controlling how CMS users interact with versioned `DataObject` records The versioned module includes a [`VersionedGridfieldDetailForm`](api:SilverStripe\Versioned\VersionedGridFieldDetailForm) extension which provides versioning support for DataObjects edited in a [`GridField`](api:SilverStripe\Forms\GridField\GridField). This is applied to [`GridFieldDetailForm`](api:SilverStripe\Forms\GridField\GridFieldDetailForm) by default. @@ -510,17 +544,17 @@ You can disable this on a per-model basis by setting the `versioned_gridfield_ex namespace App\Model; use SilverStripe\ORM\DataObject; -use SilverStripe\Versioned\Versioned; class MyBanner extends DataObject { private static $versioned_gridfield_extensions = false; + // ... } ``` -Or via yaml configuration: +Or via YAML configuration: -```yaml +```yml App\Model\MyBanner: versioned_gridfield_extensions: false ``` @@ -528,31 +562,34 @@ App\Model\MyBanner: This can also be manually enabled for a single `GridField` by passing the `VersionedGridFieldItemRequest` class name to the [`setItemRequestClass()`](api:SilverStripe\Forms\GridField\GridFieldConfig::setItemRequestClass()) method on a [`GridFieldConfig`](api:SilverStripe\Forms\GridField\GridFieldConfig) instance. ```php -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; -use SilverStripe\Forms\GridField\GridFieldDetailForm; -use SilverStripe\Versioned\VersionedGridFieldItemRequest; - -class Page extends SiteTree -{ - public function getCMSFields() - { - $fields = parent::getCMSFields(); +namespace { - $config = GridFieldConfig_RelationEditor::create(); - $config - ->getComponentByType(GridFieldDetailForm::class) - ->setItemRequestClass(VersionedGridFieldItemRequest::class); - $gridField = GridField::create('Items', 'Items', $this->Items(), $config); - $fields->addFieldToTab('Root.Items', $gridField); + use SilverStripe\CMS\Model\SiteTree; + use SilverStripe\Forms\GridField\GridField; + use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; + use SilverStripe\Forms\GridField\GridFieldDetailForm; + use SilverStripe\Versioned\VersionedGridFieldItemRequest; - return $fields; + class Page extends SiteTree + { + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $config = GridFieldConfig_RelationEditor::create(); + $config + ->getComponentByType(GridFieldDetailForm::class) + ->setItemRequestClass(VersionedGridFieldItemRequest::class); + $gridField = GridField::create('Items', 'Items', $this->Items(), $config); + $fields->addFieldToTab('Root.Items', $gridField); + + return $fields; + } } } ``` -## Interacting with versioned DataObjects +## Interacting with versioned `DataObject` records This section deals with specialised operations that can be performed on versioned DataObjects. @@ -579,15 +616,16 @@ You can also use [`Versioned::withVersionedMode()`](api:SilverStripe\Versioned\V ```php use SilverStripe\Versioned\Versioned; -$liveRecords = Versioned::withVersionedMode(function() { - // Set the reading mode we want - note we don't have to set it back afterwards, that will be done for us automatically. +$liveRecords = Versioned::withVersionedMode(function () { + // Set the reading mode we want - note we don't have to set it back afterwards, + // that will be done for us automatically. Versioned::set_stage(Versioned::LIVE); // Return the result so we can assign it to the `$liveRecords` variable. return MyRecord::get(); }); ``` -You can use `Versioned::set_stage()` outside of calls to `Versioned::withVersionedMode()`, but you _must_ remember to set the reading mode back to what is was before you started, or you risk unexpected side effects. The only exception to this is if you explicitly want the rest of the request execution to be performed with a given reading mode (e.g. if a given action on a controller must be explicitly completely executed ina given stage). +You can use `Versioned::set_stage()` outside of calls to `Versioned::withVersionedMode()`, but you *must* remember to set the reading mode back to what is was before you started, or you risk unexpected side effects. The only exception to this is if you explicitly want the rest of the request execution to be performed with a given reading mode (e.g. if a given action on a controller must be explicitly completely executed ina given stage). ```php use SilverStripe\Versioned\Versioned; @@ -625,7 +663,7 @@ $historicalRecord = Versioned::get_version(MyRecord::class, id: 5, version: 6); The record is retrieved as a regular `DataObject` record with its values set to the values it had when that version was originally saved. [alert] -Saving modifications via `write()` will create a _new_ version, rather than modifying the existing one. +Saving modifications via `write()` will create a *new* version, rather than modifying the existing one. [/alert] In order to get a list of all versions for a specific record, we get the record version data as specialized [`Versioned_Version`](api:SilverStripe\Versioned\Versioned_Version) @@ -633,39 +671,47 @@ objects, which expose the same database information as a `DataObject`, but also a record was published. ```php -$record = MyRecord::get()->byID(99); // stage doesn't matter here +// stage doesn't matter here +$record = MyRecord::get()->byID(99); $versions = $record->allVersions(); -$version = $versions->First()->Version; // instance of Versioned_Version +// instance of Versioned_Version +$version = $versions->First()->Version; ``` -### Writing changes to a versioned DataObject +### Writing changes to a versioned `DataObject` When you call the `write()` method on a versioned `DataObject` record, this will transparently create a new version of the record in the "Stage" stage. To write your changes without creating new version, call [`writeWithoutVersion()`](api:SilverStripe\Versioned\Versioned::writeWithoutVersion()) instead. ```php -$record = MyRecord::get()->byID(99); // This will retrieve the latest draft version of record ID 99. -echo $record->Version; // This will output the version ID. Let's assume it's 13. +// This will retrieve the latest draft version of record ID 99. +$record = MyRecord::get()->byID(99); +// This will output the version ID. Let's assume it's 13. +echo $record->Version; $record->Title = "Foo Bar"; -$record->write(); // This will create a new version of record ID 99. -echo $record->Version; // Will output 14 (because a new version was created). +// This will create a new version of record ID 99. +$record->write(); +// Will output 14 (because a new version was created). +echo $record->Version; $record->Title = "FOO BAR"; -$record->writeWithoutVersion(); // This will edit the latest version of record ID 99. -echo $record->Version; // Will still output 14 (because we edited the existing version). +// This will edit the latest version of record ID 99. +$record->writeWithoutVersion(); +// Will still output 14 (because we edited the existing version). +echo $record->Version; ``` -An "unpublish" operation _removes_ that record from the "Live" stage. +An "unpublish" operation *removes* that record from the "Live" stage. -### Publishing a versioned DataObject +### Publishing a versioned `DataObject` There's two main methods used to publish a versioned `DataObject` record: -* [`publishSingle()`](api:SilverStripe\Versioned\Versioned::writeWithoutVersion()) publishes _only_ this record to live from the draft -* [`publishRecursive()`](api:SilverStripe\Versioned\RecursivePublishable::publishRecursive()) publishes this record and any dependent objects this record may refer to. +- [`publishSingle()`](api:SilverStripe\Versioned\Versioned::writeWithoutVersion()) publishes *only* this record to live from the draft +- [`publishRecursive()`](api:SilverStripe\Versioned\RecursivePublishable::publishRecursive()) publishes this record and any dependent objects this record may refer to. In most regular cases, you'll want to use `publishRecursive()`. @@ -675,29 +721,32 @@ In most regular cases, you'll want to use `publishRecursive()`. $record = MyRecord::get()->byID(99); $record->MyField = 'changed'; -// Will create a new revision in "Stage". Editors will be able to see this revision, but unauthenticated visitors to the website will not see it. +// Will create a new revision in "Stage". Editors will be able to see this revision, +// but unauthenticated visitors to the website will not see it. $record->write(); // This will publish the changes so they are visible publicly. $record->publishRecursive(); ``` -### Unpublishing and archiving a versioned DataObject +### Unpublishing and archiving a versioned `DataObject` -Archiving and unpublishing are similar operations, both will prevent a versioned DataObject from being publicly accessible. Archiving will also remove the record from the "Stage" stage; other ORMs may refer to this concept as _soft-deletion_. +Archiving and unpublishing are similar operations, both will prevent a versioned DataObject from being publicly accessible. Archiving will also remove the record from the "Stage" stage; other ORMs may refer to this concept as *soft-deletion*. Both of these operations create a new entry in the relevant `_Versions` table with the `WasDeleted` column set to `1` (see [How versioned DataObjects are tracked in the database](#versions-in-the-database)). -Call [`doUnpublish()`](api:SilverStripe\Versioned\Versioned::doUnpublish()) to unpublish an item. Either call [`doArchive()`](api:SilverStripe\Versioned\Versioned::doArchive()) or simply call `delete()` to archive an item. The SilverStripe ORM doesn't allow you to _hard-delete_ versioned DataObjects. Instead they are simply removed from all stages, but all version history is retained. This allowed you to restore archived records later on, if you want to. +Call [`doUnpublish()`](api:SilverStripe\Versioned\Versioned::doUnpublish()) to unpublish an item. Either call [`doArchive()`](api:SilverStripe\Versioned\Versioned::doArchive()) or simply call `delete()` to archive an item. The Silverstripe ORM doesn't allow you to *hard-delete* versioned DataObjects. Instead they are simply removed from all stages, but all version history is retained. This allowed you to restore archived records later on, if you want to. ```php $record = MyRecord::get()->byID(99); -// Visitors to the site won't be able to see this record anymore, but editors can still edit it and re-publish it. +// Visitors to the site won't be able to see this record anymore, but editors can +// still edit it and re-publish it. $record->doUnpublish(); -// Editors won't be able to see this record anymore, but its version history will still be in the database and may be restored. +// Editors won't be able to see this record anymore, but its version history will +// still be in the database and may be restored. $record->delete(); // or $record->doArchive(); @@ -736,13 +785,12 @@ if the live version of `$record` is #10 and the staged version is #13, rolling b Archived records can still be retrieved using `get_including_deleted()`. This will include archived as well as current records. You can use the `isArchived()` method to determine if a record is archived or not. Calling the `write()` method on an archived record will restore it to the "Stage" stage. ```php -use MyRecord; +use App\Model\MyRecord; use SilverStripe\Versioned\Versioned; // This script will restore all archived entries for MyRecord. $allMyRecords = Versioned::get_including_deleted(MyRecord::class); -foreach ($allMyRecords as $myRecord) -{ +foreach ($allMyRecords as $myRecord) { if ($myRecord->isArchived()) { $myRecord->write(); } @@ -751,43 +799,43 @@ foreach ($allMyRecords as $myRecord) If you already know a specific record was archived and want to restor it, you can also use the `rollbackRecursive()` and `rollbackSingle()` methods - but you still have to get a hold of the archived record using `get_including_deleted()` first. -## Interacting with ChangeSet +## Interacting with `ChangeSet` This section explains how you can interact with ChangeSets. -### Adding and removing DataObjects to a change set +### Adding and removing `DataObject` records to a change set -* `$myChangeSet->addObject(DataObject $record)`: Add a record and all of its owned records to the changeset (`canEdit()` dependent). -* `$myChangeSet->removeObject(DataObject $record)`: Removes a record and all of its owned records from the changeset (`canEdit()` dependent). +- `$myChangeSet->addObject(DataObject $record)`: Add a record and all of its owned records to the changeset (`canEdit()` dependent). +- `$myChangeSet->removeObject(DataObject $record)`: Removes a record and all of its owned records from the changeset (`canEdit()` dependent). -### Performing actions on the ChangeSet object +### Performing actions on the `ChangeSet` object -* `$myChangeSet->publish()`: Publishes all items in the changeset that have modifications, along with all their owned records (`canPublish()` dependent). Closes the changeset on completion. -* `$myChangeSet->sync()`: Find all owned records with modifications for each item in the changeset, and include them implicitly. -* `$myChangeSet->validate()`: Ensure all owned records with modifications for each item in the changeset are included. This method should not need to be invoked if `sync()` is being used on each mutation to the changeset. +- `$myChangeSet->publish()`: Publishes all items in the changeset that have modifications, along with all their owned records (`canPublish()` dependent). Closes the changeset on completion. +- `$myChangeSet->sync()`: Find all owned records with modifications for each item in the changeset, and include them implicitly. +- `$myChangeSet->validate()`: Ensure all owned records with modifications for each item in the changeset are included. This method should not need to be invoked if `sync()` is being used on each mutation to the changeset. -### Getting information about the state of the ChangeSet +### Getting information about the state of the `ChangeSet` ChangeSets can exists in three different states: -* `open` No action has been taken on the ChangeSet. Resolves to `publishing` or `reverting`. -* `published`: The ChangeSet has published changes to all of its items and its now closed. -* `reverted`: The ChangeSet has reverted changes to all of its items and its now closed. (Future API, not supported yet) +- `open` No action has been taken on the ChangeSet. Resolves to `publishing` or `reverting`. +- `published`: The ChangeSet has published changes to all of its items and its now closed. +- `reverted`: The ChangeSet has reverted changes to all of its items and its now closed. (Future API, not supported yet) -### Getting information about items in a ChangeSet +### Getting information about items in a `ChangeSet` Each item in the ChangeSet stores `VersionBefore` and `VersionAfter` fields. As such, they can compute the type of change they are adding to their parent ChangeSet. Change types include: -* `created`: This ChangeSet item is for a record that does not yet exist -* `modified`: This ChangeSet item is for a record that differs from what is on the live stage -* `deleted`: This ChangeSet item will no longer exist when the ChangeSet is published -* `none`: This ChangeSet item is exactly as it is on the live stage +- `created`: This ChangeSet item is for a record that does not yet exist +- `modified`: This ChangeSet item is for a record that differs from what is on the live stage +- `deleted`: This ChangeSet item will no longer exist when the ChangeSet is published +- `none`: This ChangeSet item is exactly as it is on the live stage ## Advanced versioning topics These topics are targeted towards more advanced use cases that might require developers to extend the behavior of versioning. -### How versioned DataObjects are tracked in the database {#versions-in-the-database} +### How versioned `DataObject` records are tracked in the database {#versions-in-the-database} Depending on whether staging is enabled, one or more new tables will be created for your records. `_Versions` is always created to track historic versions for your model. If staging is enabled this will also create a new @@ -797,18 +845,18 @@ is always created to track historic versions for your model. If staging is enabl Note that the "Stage" stage doesn't get its own table - instead, the original table represents the "Stage" stage. [/notice] - * `MyRecord` table: Contains "Stage" (draft) data - * `MyRecord_Live` table: Contains "Live" (published) data - * `MyRecord_Versions` table: Contains a version history (new row created on each save, publish, unpublish, archive, and rollback event) +- `MyRecord` table: Contains "Stage" (draft) data +- `MyRecord_Live` table: Contains "Live" (published) data +- `MyRecord_Versions` table: Contains a version history (new row created on each save, publish, unpublish, archive, and rollback event) Similarly, any subclass you create of a versioned `DataObject` will trigger the creation of additional tables, which are automatically joined as required: - * `MyRecordSubclass` table: Contains only "Stage" (draft) data for subclass columns - * `MyRecordSubclass_Live` table: Contains only "Live" (published) data for subclass columns - * `MyRecordSubclass_Versions` table: Contains only version history for subclass columns +- `MyRecordSubclass` table: Contains only "Stage" (draft) data for subclass columns +- `MyRecordSubclass_Live` table: Contains only "Live" (published) data for subclass columns +- `MyRecordSubclass_Versions` table: Contains only version history for subclass columns -### Writing custom queries to retrieve versioned DataObject +### Writing custom queries to retrieve versioned `DataObject` We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining multiple tables across an inherited table scheme (see [Versioned::augmentSQL()](api:SilverStripe\Versioned\Versioned::augmentSQL())). If possible, try to stick to smaller modifications of the generated `DataList` objects. @@ -840,23 +888,22 @@ Settin `Versioned.use_session` can lead to leaking unpublished information, e.g. and the result is cached due to aggressive cache settings (not varying on cookie values). [/warning] -*app/src/MyObject.php* - ```php +// app/src/Model/MyObject.php namespace App\Model; -use App\Controller\MyObjectController; +use App\Control\MyObjectController; +use SilverStripe\Control\Controller; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; -use SilverStripe\Control\Controller; class MyObject extends DataObject { - private static $extensions = [ - Versioned::class + Versioned::class, ]; + // ... public function Link() { @@ -867,7 +914,8 @@ class MyObject extends DataObject { $link = Controller::join_links('custom-route', $this->ID, '?rand=' . rand()); // Calls VersionedStateExtension->updateLink() which ensures the correct stage is included if necessary - $this->extend('updateLink', $link); // updates $link by reference + // updates $link by reference + $this->extend('updateLink', $link); return $link; } @@ -884,10 +932,9 @@ class MyObject extends DataObject } ``` -*app/src/MyObjectController.php* - ```php -namespace App\Controller; +// app/src/Control/MyObjectController.php +namespace App\Control; use App\Model\MyObject; use SilverStripe\Control\Controller; @@ -895,6 +942,8 @@ use SilverStripe\Control\HTTPRequest; class MyObjectController extends Controller { + private static $url_segment = 'my-objects'; + public function index(HTTPRequest $request) { $obj = MyObject::get()->byID($request->param('ID')); @@ -922,12 +971,11 @@ class MyObjectController extends Controller } ``` -*app/_config/routes.yml* - -```yaml +```yml +# app/_config/routes.yml SilverStripe\Control\Director: rules: - 'my-objects/$ID': 'App\Controller\MyObjectController' + 'my-objects/$ID': 'App\Control\MyObjectController' ``` [alert] @@ -936,22 +984,22 @@ authenticated to view it. As with any other controller logic, please use `DataOb permissions, and avoid exposing unpublished content to your users. [/alert] -#### Templates Variables +#### Templates variables In templates, you don't need to worry about this distinction. The `$Content` variable contains the published content by default, and previews draft content only if explicitly requested (e.g. by the "preview" feature in the CMS, or by adding ?stage=Stage to the URL). If you want to force a specific stage, we recommend the `Controller->init()` method for this purpose, for example: -**app/src/MyController.php** - ```php -namespace App\Controller; +// app/src/Control/MyController.php +namespace App\Control; use SilverStripe\Control\Controller; -use SilverStripe\Versioned\Versioned; class MyController extends Controller { + // ... + public function init() { parent::init(); @@ -971,11 +1019,16 @@ To move a saved version from one stage to another, call [`writeToStage()`](api:S The current stage is stored as global state on the `Versioned` object. It is usually modified by controllers, e.g. when a preview is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage. ```php -$origMode = Versioned::get_reading_mode(); // save current mode -$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records -Versioned::set_reading_mode(Versioned::DRAFT); // temporarily overwrite mode -$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records -Versioned::set_reading_mode($origMode); // reset current mode +// save current mode +$origMode = Versioned::get_reading_mode(); +// returns 'Live' records +$obj = MyRecord::getComplexObjectRetrieval(); +// temporarily overwrite mode +Versioned::set_reading_mode(Versioned::DRAFT); +// returns 'Stage' records +$obj = MyRecord::getComplexObjectRetrieval(); +// reset current mode +Versioned::set_reading_mode($origMode); ``` See [Reading versions by stage](#reading-versions-by-stage) for more about using reading modes. @@ -987,17 +1040,17 @@ comparisons for a versioned DataObject. This is automatically enabled for SiteTr [dnadesign/silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental). [warning] -Because of the lack of specificity in the `HistoryViewer.Form_ItemEditForm` scope used when injecting the history viewer to the DOM, only one model can have a working history panel at a time, with exception to `SiteTree` which has its own history viewer scope. For example, if you already have `dnadesign/silverstripe-elemental` installed, the custom history viewer instance injected as a part of this documentation will _break_ the one provided by the elemental module. +Because of the lack of specificity in the `HistoryViewer.Form_ItemEditForm` scope used when injecting the history viewer to the DOM, only one model can have a working history panel at a time, with exception to `SiteTree` which has its own history viewer scope. For example, if you already have `dnadesign/silverstripe-elemental` installed, the custom history viewer instance injected as a part of this documentation will *break* the one provided by the elemental module. There are ways you can get around this limitation. You may wish to put some conditional logic in `app/client/src/boot/index.js` below to only perform the transformations if the current location is within a specific model admin, for example. [/warning] If you want to enable the history viewer for a custom versioned DataObject, you will need to: -* Expose GraphQL scaffolding -* Add the necessary GraphQL queries and mutations to your module -* Register your GraphQL queries and mutations with Injector -* Add a HistoryViewerField to the DataObject's `getCMSFields` +- Expose GraphQL scaffolding +- Add the necessary GraphQL queries and mutations to your module +- Register your GraphQL queries and mutations with Injector +- Add a HistoryViewerField to the DataObject's `getCMSFields` [notice] **Please note:** these examples are given in the context of project-level customisation. You may need to adjust @@ -1006,7 +1059,7 @@ the webpack configuration slightly for use in a module. ### Setup {#history-viewer-setup} -This example assumes you have some `DataObject` model and somewhere to view that model (e.g. in a `ModelAdmin`). We'll walk you through the steps required to add some javascript to tell the history viewer how to handle requests for your model. +This example assumes you have some `DataObject` model and somewhere to view that model (e.g. in a `ModelAdmin`). We'll walk you through the steps required to add some JavaScript to tell the history viewer how to handle requests for your model. For this example we'll start with this simple `DataObject`: @@ -1027,12 +1080,13 @@ class MyVersionedObject extends DataObject private static $extensions = [ Versioned::class, ]; + // ... } ``` #### Configure frontend asset building {#history-viewer-js} -If you haven't already configured frontend asset (javascript/css) building for your project, you will need to configure some basic +If you haven't already configured frontend asset (JavaScript/CSS) building for your project, you will need to configure some basic packages to be built in order to enable history viewer functionality. This section includes a very basic webpack configuration which uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). [hint] @@ -1046,9 +1100,8 @@ Using `@silverstripe/webpack-config` will keep your transpiled bundle size small You can configure your directory structure like so: -**package.json** - ```json +// package.json { "name": "my-project", "scripts": { @@ -1070,9 +1123,8 @@ You can configure your directory structure like so: } ``` -**webpack.config.js** - ```js +// webpack.config.js const Path = require('path'); const { JavascriptWebpackConfig } = require('@silverstripe/webpack-config'); @@ -1091,16 +1143,16 @@ module.exports = [ ]; ``` -**app/client/src/boot/index.js** - ```js -console.log('Hello world'); +// app/client/src/boot/index.js + +// We'll populate this file later - for now we just need it to be sure our build setup works. ``` At this stage, running `yarn build` should correctly build `app/client/dist/js/bundle.js`. [notice] -Don't forget to [configure your project's "exposed" folders](/developer_guides/templates/requirements/#configuring-your-project-exposed-folders) and run `composer vendor-expose` on the command line so that the browser has access to your new dist js file. +Don't forget to [configure your project's "exposed" folders](/developer_guides/templates/requirements/#configuring-your-project-exposed-folders) and run `composer vendor-expose` on the command line so that the browser has access to your new dist JS file. [/notice] ### Create and use GraphQL schema {#history-viewer-gql} @@ -1113,9 +1165,8 @@ Only a minimal amount of data is required to be exposed via GraphQL scaffolding, For more information, see [Working with DataObjects - Adding DataObjects to the schema](/developer_guides/graphql/working_with_dataobjects/adding_dataobjects_to_the_schema/). -**app/_config/graphql.yml** - -```yaml +```yml +# app/_config/graphql.yml SilverStripe\GraphQL\Schema\Schema: schemas: admin: @@ -1123,9 +1174,8 @@ SilverStripe\GraphQL\Schema\Schema: - app/_graphql ``` -**app/_graphql/models.yml** - -```yaml +```yml +# app/_graphql/models.yml App\Model\MyVersionedObject: fields: '*' operations: @@ -1134,27 +1184,26 @@ App\Model\MyVersionedObject: ``` Once configured, flush your cache and run `dev/graphql/build` either in your browser or via sake, and explore the new GraphQL schema to ensure it loads correctly. -You can use a GraphQL application such as GraphiQL, or [silverstripe-graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools) +You can use a GraphQL application such as GraphiQL, or [`silverstripe/graphql-devtools`](https://github.com/silverstripe/silverstripe-graphql-devtools) to view the schema and run queries from your browser: ```bash composer require --dev silverstripe/graphql-devtools dev-master ``` -#### Use the GraphQL query and mutation in javascript +#### Use the GraphQL query and mutation in JavaScript The history viewer interface uses two main operations: -* Read a list of versions for a DataObject -* Revert (aka rollback) to an older version of a DataObject +- Read a list of versions for a DataObject +- Revert (aka rollback) to an older version of a DataObject -`silverstripe/versioned` provides some graphql plugins we're taking advantage of here. See [Working with DataObjects - Versioned content](/developer_guides/graphql/working_with_dataobjects/versioning/) for more information. +`silverstripe/versioned` provides some GraphQL plugins we're taking advantage of here. See [Working with DataObjects - Versioned content](/developer_guides/graphql/working_with_dataobjects/versioning/) for more information. For this we need one query and one mutation: -**app/client/src/state/readOneMyVersionedObjectQuery.js** - ```js +// app/client/src/state/readOneMyVersionedObjectQuery.js import { graphql } from '@apollo/client/react/hoc'; import gql from 'graphql-tag'; @@ -1258,12 +1307,10 @@ const config = { export { query, config }; export default graphql(query, config); - ``` -**app/client/src/state/revertToMyVersionedObjectVersionMutation.js** - ```js +// app/client/src/state/revertToMyVersionedObjectVersionMutation.js import { graphql } from '@apollo/client/react/hoc'; import gql from 'graphql-tag'; @@ -1308,18 +1355,18 @@ export { mutation, config }; export default graphql(mutation, config); ``` -#### Register your GraphQL query and mutation with Injector +#### Register your GraphQL query and mutation with `Injector` Once your GraphQL query and mutation are created you will need to tell the JavaScript Injector about them. This does two things: -* Allow them to be loaded by core components. -* Allow Injector to provide them in certain contexts. They should be available for `MyVersionedObject` history viewer +- Allow them to be loaded by core components. +- Allow Injector to provide them in certain contexts. They should be available for `MyVersionedObject` history viewer instances, but not for CMS pages for example. -**app/client/src/boot/index.js** - ```js +// app/client/src/boot/index.js + /* global window */ import Injector from 'lib/Injector'; import readOneMyVersionedObjectQuery from 'state/readOneMyVersionedObjectQuery'; @@ -1356,7 +1403,7 @@ window.document.addEventListener('DOMContentLoaded', () => { For more information, see [Using Injector to customise GraphQL queries](/developer_guides/customising_the_admin_interface/react_redux_and_graphql#using-injector-to-customise-graphql-queries) and [Transforming services using middleware](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#transforming-services-using-middleware). -### Adding the HistoryViewerField +### Adding the `HistoryViewerField` Firstly, ensure your JavaScript bundle is included throughout the CMS: @@ -1388,14 +1435,14 @@ public function getCMSFields() } ``` -### Previewable DataObjects +### Previewable `DataObject` models The history viewer will automatically detect and render a side-by-side preview panel for DataObjects that implement [CMSPreviewable](api:SilverStripe\ORM\CMSPreviewable). Please note that if you are adding this functionality, you will also need to expose the `AbsoluteLink` field in your GraphQL read scaffolding, and add it to the fields in `readOneMyVersionedObjectQuery`. -## API Documentation +## API documentation -* [Versioned](api:SilverStripe\Versioned\Versioned) -* [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField) +- [Versioned](api:SilverStripe\Versioned\Versioned) +- [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField) diff --git a/en/02_Developer_Guides/00_Model/11_Scaffolding.md b/en/02_Developer_Guides/00_Model/11_Scaffolding.md index b6d739b3c..686d2d79f 100644 --- a/en/02_Developer_Guides/00_Model/11_Scaffolding.md +++ b/en/02_Developer_Guides/00_Model/11_Scaffolding.md @@ -6,16 +6,18 @@ icon: hammer # Scaffolding -The ORM already has a lot of information about the data represented by a `DataObject` through its `$db` property, so +The ORM already has a lot of information about the data represented by a `DataObject` through its `$db` property, so Silverstripe CMS will use that information to scaffold some interfaces. This is done though [FormScaffolder](api:SilverStripe\Forms\FormScaffolder) -to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans). You can then further +to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans). You can then further customise those fields as required. -## Form Fields +## Form fields An example is `DataObject`, Silverstripe CMS will automatically create your CMS interface so you can modify what you need, without having to define all of your form fields from scratch. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject @@ -23,15 +25,15 @@ class MyDataObject extends DataObject private static $db = [ 'IsActive' => 'Boolean', 'Title' => 'Varchar', - 'Content' => 'Text' + 'Content' => 'Text', ]; - public function getCMSFields() + public function getCMSFields() { // parent::getCMSFields() does all the hard work and creates the fields for Title, IsActive and Content. $fields = parent::getCMSFields(); $fields->dataFieldByName('IsActive')->setTitle('Is active?'); - + return $fields; } } @@ -44,22 +46,33 @@ It is typically considered a good practice to wrap your modifications in a call To fully customise your form fields, start with an empty FieldList. ```php -public function getCMSFields() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyDataObject extends DataObject { - $fields = FieldList::create( - TabSet::create("Root", - Tab::create("Main", - CheckboxSetField::create('IsActive','Is active?'), - TextField::create('Title'), - TextareaField::create('Content') - ->setRows(5) + // ... + + public function getCMSFields() + { + $fields = FieldList::create( + TabSet::create( + 'Root', + Tab::create( + 'Main', + CheckboxSetField::create('IsActive', 'Is active?'), + TextField::create('Title'), + TextareaField::create('Content') + ->setRows(5) + ) ) - ) - ); + ); - $this->extend('updateCMSFields', $fields); - - return $fields; + $this->extend('updateCMSFields', $fields); + + return $fields; + } } ``` @@ -73,7 +86,7 @@ You can also alter the fields of built-in and module `DataObject` classes by imp `FormField` scaffolding takes [`$field_labels` config](#field-labels) into account as well. [/info] -## Searchable Fields +## Searchable fields The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin system. The default is a set of array values listing the fields. @@ -83,19 +96,20 @@ system. The default is a set of array values listing the fields. [/info] [warning] -If you define a `searchable_fields` configuration, _do not_ specify fields that are not stored in the database (such as methods), as this will cause an error. +If you define a `searchable_fields` configuration, *do not* specify fields that are not stored in the database (such as methods), as this will cause an error. [/warning] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { - - private static $searchable_fields = [ + private static $searchable_fields = [ 'Name', - 'ProductCode' - ]; + 'ProductCode', + ]; } ``` @@ -105,9 +119,11 @@ Tabular views such as `GridField` or `ModelAdmin` include a search bar. The sear #### Exclude fields from the general search -If you have fields which you do _not_ want to be searched with this general search (e.g. date fields which need special consideration), you can mark them as being explicitly excluded by setting `general` to false in the searchable fields configuration for that field: +If you have fields which you do *not* want to be searched with this general search (e.g. date fields which need special consideration), you can mark them as being explicitly excluded by setting `general` to false in the searchable fields configuration for that field: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject @@ -115,7 +131,7 @@ class MyDataObject extends DataObject private static $searchable_fields = [ 'Name', 'BirthDate' => [ - 'general' => false + 'general' => false, ], ]; } @@ -129,14 +145,18 @@ By default the general search field uses the name "q". If you already use that f If you set `general_search_field_name` to any empty string, general search will be disabled entirely. Instead, the first field in your searchable fields configuration will be used. [/hint] -**Globally change the general search field name via yaml config** +##### Globally change the general search field name via YAML config {#general-field-name-yaml} + ```yml SilverStripe\ORM\DataObject: general_search_field_name: 'my_general_field_name' ``` -**Customise the general search field name via yaml _or_ php config** +##### Customise the general search field name via YAML *or* PHP config {#general-field-name-php} + ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject @@ -151,9 +171,10 @@ By default, the general search will search across your fields using a [PartialMa You can configure this to be a specific filter class, or else disable the general search filter. Disabling the filter will result in the filters you have specified for each field being used when searching against that field in the general search. -Like the general search field name, you can set this either globally or per class: +Like the general search field name, you can set this either globally or per class. + +##### Globally change the general search filter via YAML config {#general-field-filter-yaml} -**Globally change the general search filter via yaml config** ```yml # use a specific filter SilverStripe\ORM\DataObject: @@ -164,8 +185,11 @@ SilverStripe\ORM\DataObject: general_search_field_filter: '' ``` -**Customise the general search filter via yaml _or_ php config** +##### Customise the general search filter via YAML *or* PHP config {#general-field-filter-php} + ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Filters\EndsWithFilter; @@ -176,28 +200,33 @@ class MyDataObject extends DataObject ``` [warning] -You may get unexpected results using some filters if you don't disable splitting the query into terms - for example if you use an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter), each term in the query _must_ exactly match the value in at least one field to get a match. If you disable splitting terms, the whole query must exactly match a field value instead. +You may get unexpected results using some filters if you don't disable splitting the query into terms - for example if you use an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter), each term in the query *must* exactly match the value in at least one field to get a match. If you disable splitting terms, the whole query must exactly match a field value instead. [/warning] #### Splitting search queries into individual terms By default the general search field will split your search query on spaces into individual terms, and search across your searchable field for each term. At least one field must match each term to get a match. -For example: with the search query "farm house" at least one field must have a match for the word "farm", and at least one field must have a match for the word "house". There does _not_ need to be a field which matches the full phrase "farm house". +For example: with the search query "farm house" at least one field must have a match for the word "farm", and at least one field must have a match for the word "house". There does *not* need to be a field which matches the full phrase "farm house". You can disable this behaviour by setting `DataObject.general_search_split_terms` to false. This would mean that for the example above a `DataObject` would need a field that matches "farm house" to be included in the results. Simply matching "farm" or "house" alone would not be sufficient. -Like the general search field name, you can set this either globally or per class: +Like the general search field name, you can set this either globally or per class. + +##### Globally disable splitting terms via YAML config {#general-field-split-yaml} -**Globally disable splitting terms via yaml config** ```yml SilverStripe\ORM\DataObject: general_search_split_terms: false ``` -**Disable splitting terms via yaml _or_ php config** +##### Disable splitting terms via YAML *or* PHP config {#general-field-split-php} + ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; + class MyDataObject extends DataObject { private static bool $general_search_split_terms = false; @@ -206,16 +235,23 @@ class MyDataObject extends DataObject #### Use a specific single field -If you disable the global general search functionality, the general seach field will revert to searching against the _first -field_ in your `searchableFields` list. +If you disable the global general search functionality, the general seach field will revert to searching against the *first +field* in your `searchableFields` list. As an example, let's look at a definition like this: ```php -private static $searchable_fields = [ - 'Name', - 'JobTitle', -]; +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyDataObject extends DataObject +{ + private static $searchable_fields = [ + 'Name', + 'JobTitle', + ]; +} ``` That `Name` comes first in that list is actually quite a good thing. The user will likely want the @@ -225,17 +261,31 @@ like `JobTitle`. By contrast, let's look at this definition: ```php -private static $searchable_fields = [ - 'Price', - 'Description', - 'Title', -]; +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyDataObject extends DataObject +{ + private static $searchable_fields = [ + 'Price', + 'Description', + 'Title', + ]; +} ``` It's unlikely that the user will want to search on `Price`. A better candidate would be `Title` or `Description`. Rather than reorder the array, which may be counter-intuitive, you can use the `general_search_field` configuration property. ```php -private static $general_search_field = 'Title'; +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyDataObject extends DataObject +{ + private static $general_search_field = 'Title'; +} ``` ##### Customise the field per `GridField` @@ -250,18 +300,21 @@ This is useful if you have disabled the global general search functionality, if ### Specify a form field or search filter -Searchable fields will appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a +Searchable fields will appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a default search filter assigned (usually a [PartialMatchFilter](api:SilverStripe\ORM\Filters\PartialMatchFilter)). To override these defaults, you can specify additional information on `$searchable_fields`: ```php +namespace App\Model; + +use SilverStripe\Forms\NumericField; use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { private static $searchable_fields = [ 'Name' => 'PartialMatchFilter', - 'ProductCode' => NumericField::class + 'ProductCode' => NumericField::class, ]; } ``` @@ -270,11 +323,15 @@ If you assign a single string value, you can set it to be either a [FormField](a both or to combine this with other configuration, you can assign an array: ```php +namespace App\Model; + +use SilverStripe\Forms\NumericField; +use SilverStripe\Forms\TextField; use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { - private static $searchable_fields = [ + private static $searchable_fields = [ 'Name' => [ 'field' => TextField::class, 'filter' => 'PartialMatchFilter', @@ -284,7 +341,7 @@ class MyDataObject extends DataObject 'field' => NumericField::class, 'filter' => 'PartialMatchFilter', ], - ]; + ]; } ``` @@ -293,36 +350,43 @@ class MyDataObject extends DataObject To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Team extends DataObject +class Team extends DataObject { private static $db = [ - 'Title' => 'Varchar' + 'Title' => 'Varchar', ]; - + private static $many_many = [ - 'Players' => 'Player' + 'Players' => 'Player', ]; - + private static $searchable_fields = [ 'Title', 'Players.Name', ]; } +``` + +```php +namespace App\Model; -class Player extends DataObject +use SilverStripe\ORM\DataObject; + +class Player extends DataObject { private static $db = [ 'Name' => 'Varchar', 'Birthday' => 'Date', ]; - + private static $belongs_many_many = [ - 'Teams' => 'Team' + 'Teams' => 'Team', ]; } - ``` ### Searching many db fields on a single search field @@ -334,6 +398,10 @@ If you don't specify a `FormField`, you must use the name of a real database fie [/alert] ```php +namespace App\Model; + +use SilverStripe\Forms\TextField; + class Order extends DataObject { private static $db = [ @@ -350,22 +418,25 @@ class Order extends DataObject 'title' => 'First Name', 'field' => TextField::class, 'match_any' => [ - // Searching with the "First Name" field will show Orders matching either Name, Customer.FirstName, or ShippingAddress.FirstName + // Searching with the "First Name" field will show Orders matching either + // Name, Customer.FirstName, or ShippingAddress.FirstName 'Name', 'Customer.FirstName', 'ShippingAddress.FirstName', - ] - ] + ], + ], ]; } ``` -## Summary Fields +## Summary fields -Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use +Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject @@ -374,8 +445,8 @@ class MyDataObject extends DataObject 'Name' => 'Text', 'OtherProperty' => 'Text', 'ProductCode' => 'Int', - ]; - + ]; + private static $summary_fields = [ 'Name', 'ProductCode', @@ -388,14 +459,22 @@ class MyDataObject extends DataObject To include relations or field manipulations in your summaries, you can use a dot-notation. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class OtherObject extends DataObject -{ +class OtherObject extends DataObject +{ private static $db = [ 'Title' => 'Varchar', ]; } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { @@ -403,18 +482,17 @@ class MyDataObject extends DataObject 'Name' => 'Text', 'Description' => 'HTMLText', ]; - + private static $has_one = [ 'OtherObject' => 'OtherObject', ]; - + private static $summary_fields = [ 'Name' => 'Name', 'Description.Summary' => 'Description (summary)', 'OtherObject.Title' => 'Other Object Title', ]; } - ``` ### Images in summary fields @@ -422,24 +500,25 @@ class MyDataObject extends DataObject Non-textual elements (such as images and their manipulations) can also be used in summaries. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject -{ +{ private static $db = [ 'Name' => 'Text', ]; - + private static $has_one = [ 'HeroImage' => 'Image', ]; - + private static $summary_fields = [ 'Name' => 'Name', 'HeroImage.CMSThumbnail' => 'Hero Image', ]; } - ``` ## Field labels @@ -452,20 +531,20 @@ namespace App\Model; use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject -{ +{ private static $db = [ 'Name' => 'Text', ]; - + private static $has_one = [ 'HeroImage' => Image::class, ]; - + private static $summary_fields = [ 'Name', 'HeroImage.CMSThumbnail', ]; - + private static $field_labels = [ 'Name' => 'Name', 'HeroImage.CMSThumbnail' => 'Hero', @@ -475,15 +554,14 @@ class MyDataObject extends DataObject ### Localisation {#field-label-localisation} -For any fields _not_ defined in `$field_labels`, labels can be localised by defining the name prefixed by the type of field (e.g `db_`, `has_one_`, etc) in your localisation yaml files: +For any fields *not* defined in `$field_labels`, labels can be localised by defining the name prefixed by the type of field (e.g `db_`, `has_one_`, etc) in your localisation YAML files: [info] The class name should be the class that defined the field or relationship. [/info] -**`app/lang/en.yml`** - ```yml +# app/lang/en.yml en: App\Model\MyDataObject: db_Name: "Name" @@ -491,32 +569,41 @@ en: ``` [notice] -For relations (such as `has_one_HeroImage` above), this field label applies to the scaffolded form field (an `UploadField` for files, a tab for `has_many`/`many_many`, etc). It does _not_ apply to summary or searchable fields with dot notation. +For relations (such as `has_one_HeroImage` above), this field label applies to the scaffolded form field (an `UploadField` for files, a tab for `has_many`/`many_many`, etc). It does *not* apply to summary or searchable fields with dot notation. [/notice] -Labels you define in `$field_labels` _won't_ be overridden by localisation strings. To make those localisable, you will need to override the [`fieldLabels()`](api:SilverStripe\ORM\DataObject) method and explicitly localise those labels yourself: +Labels you define in `$field_labels` *won't* be overridden by localisation strings. To make those localisable, you will need to override the [`fieldLabels()`](api:SilverStripe\ORM\DataObject) method and explicitly localise those labels yourself: ```php -public function fieldLabels($includerelations = true) +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyDataObject extends DataObject { - $labels = parent::fieldLabels($includerelations); - $customLabels = static::config()->get('field_labels'); + // ... - foreach ($customLabels as $name => $label) { - $labels[$name] = _t(__CLASS__ . '.' . $name, $label); - } + public function fieldLabels($includerelations = true) + { + $labels = parent::fieldLabels($includerelations); + $customLabels = static::config()->get('field_labels'); + + foreach ($customLabels as $name => $label) { + $labels[$name] = _t(__CLASS__ . '.' . $name, $label); + } - return $labels; + return $labels; + } } ``` See the [i18n section](/developer_guides/i18n) for more about localisation. -## Related Documentation +## Related documentation -* [SearchFilters](searchfilters) +- [SearchFilters](searchfilters) -## API Documentation +## API documentation -* [FormScaffolder](api:SilverStripe\Forms\FormScaffolder) -* [DataObject](api:SilverStripe\ORM\DataObject) +- [FormScaffolder](api:SilverStripe\Forms\FormScaffolder) +- [DataObject](api:SilverStripe\ORM\DataObject) diff --git a/en/02_Developer_Guides/00_Model/12_Indexes.md b/en/02_Developer_Guides/00_Model/12_Indexes.md index fd5b9d7ba..29fc00d1d 100644 --- a/en/02_Developer_Guides/00_Model/12_Indexes.md +++ b/en/02_Developer_Guides/00_Model/12_Indexes.md @@ -5,38 +5,44 @@ icon: database --- # Indexes -Indexes are a great way to improve performance in your application, especially as it grows. By adding indexes to your -data model you can reduce the time taken for the framework to find and filter data objects. -The addition of an indexes should be carefully evaluated as they can also increase the cost of other operations such as +Indexes are a great way to improve performance in your application, especially as it grows. By adding indexes to your +data model you can reduce the time taken for the framework to find and filter data objects. + +The addition of an indexes should be carefully evaluated as they can also increase the cost of other operations such as `UPDATE`/`INSERT` and `DELETE`. An index on a column whose data is non unique will actually cost you performance. -E.g. In most cases an index on `boolean` status flag, or `ENUM` state will not increase query performance. +For example, in most cases an index on `boolean` status flag, or `ENUM` state will not increase query performance. + +It's important to find the right balance to achieve fast queries using the optimal set of indexes; For Silverstripe CMS +applications it's a good practice to: -It's important to find the right balance to achieve fast queries using the optimal set of indexes; For Silverstripe CMS -applications it's a good practice to: - add indexes on columns which are frequently used in `filter`, `where` or `orderBy` statements - for these, only include indexes for columns which are the most restrictive (return the least number of rows) The Silverstripe CMS framework already places certain indexes for you by default: + - The primary key for each model has a `PRIMARY KEY` unique index - The `ClassName` column if your model inherits from `DataObject` -- All relationships defined in the model have indexes for their `has_one` entity (for `many_many` relationships +- All relationships defined in the model have indexes for their `has_one` entity (for `many_many` relationships this index is present on the associative entity). - All fields used in `default_sort` configuration ## Defining an index -Indexes are represented on a `DataObject` through the `DataObject.indexes` configuration property which maps index names to a + +Indexes are represented on a `DataObject` through the [`DataObject.indexes`](api:SilverStripe\ORM\DataObject->indexes) configuration property which maps index names to a descriptor. There are several supported notations: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyObject extends DataObject +class MyObject extends DataObject { private static $indexes = [ '' => true, '' => [ - 'type' => '', + 'type' => '', 'columns' => ['', ''], ], '' => ['', ''], @@ -44,23 +50,24 @@ class MyObject extends DataObject } ``` -The `` is used to put a standard non-unique index on the column specified. For complex or large tables +The `` is used to put a standard non-unique index on the column specified. For complex or large tables we recommend building the index to suite the requirements of your data. -The `` can be an arbitrary identifier in order to allow for more than one index on a specific database -column. The "advanced" notation supports more `` notations. These vary between database drivers, but all of them +The `` can be an arbitrary identifier in order to allow for more than one index on a specific database +column. The "advanced" notation supports more `` notations. These vary between database drivers, but all of them support the following: - * `index`: Standard non unique index. - * `unique`: Index plus uniqueness constraint on the value - * `fulltext`: Fulltext content index - -**app/src/MyTestObject.php** +- `index`: Standard non unique index. +- `unique`: Index plus uniqueness constraint on the value +- `fulltext`: Fulltext content index ```php +// app/src/MyTestObject.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyTestObject extends DataObject +class MyTestObject extends DataObject { private static $db = [ 'MyField' => 'Varchar', @@ -73,25 +80,25 @@ class MyTestObject extends DataObject } ``` -## Complex/Composite Indexes -For complex queries it may be necessary to define a complex or composite index on the supporting object. To create a -composite index, define the fields in the index order as a comma separated list. +## Complex/Composite indexes + +For complex queries it may be necessary to define a complex or composite index on the supporting object. To create a +composite index, define the fields in the index order as a comma separated list. -*Note* Most databases only use the leftmost prefix to optimise the query, try to ensure the order of the index and your -query parameters are the same. e.g. - index (col1) - `WHERE col1 = ?` - index (col1, col2) = `WHERE (col1 = ? AND col2 = ?)` - index (col1, col2, col3) = `WHERE (col1 = ? AND col2 = ? AND col3 = ?)` The index would not be used for a query `WHERE col2 = ?` or for `WHERE col1 = ? OR col2 = ?` -As an alternative to a composite index, you can also create a hashed column which is a combination of information from -other columns. If this is indexed, smaller and reasonably unique it might be faster that an index on the whole column. +As an alternative to a composite index, you can also create a hashed column which is a combination of information from +other columns. If this is indexed, smaller and reasonably unique it might be faster that an index on the whole column. + +## Index creation/destruction -## Index Creation/Destruction -Indexes are generated and removed automatically during a `dev/build`. Caution if you're working with large tables and -modify an index as the next `dev/build` will `DROP` the index, and then `ADD` it. +Indexes are generated and removed automatically during a `dev/build`. Caution if you're working with large tables and +modify an index as the next `dev/build` will `DROP` the index, and then `ADD` it. -## API Documentation +## API documentation -* [DataObject](api:SilverStripe\ORM\DataObject) +- [DataObject](api:SilverStripe\ORM\DataObject) diff --git a/en/02_Developer_Guides/00_Model/13_Managing_Records.md b/en/02_Developer_Guides/00_Model/13_Managing_Records.md index f40130593..286b79683 100644 --- a/en/02_Developer_Guides/00_Model/13_Managing_Records.md +++ b/en/02_Developer_Guides/00_Model/13_Managing_Records.md @@ -4,7 +4,7 @@ summary: Manage your DataObject records icon: list-alt --- -# Managing Records +# Managing records Most records in Silverstripe CMS are managed [in a GridField](../forms/field_types/gridfield) - whether in the [GridField](api:SilverStripe\Forms\GridField\GridField) of some other record or directly [in a ModelAdmin](../customising_the_admin_interface/modeladmin/). The notable exceptions to this are @@ -21,14 +21,14 @@ When using this extension, your model must also declare its `cms_edit_owner` as [configuration property](../configuration/configuration/#configuration-properties). The value must either be the class name of the `ModelAdmin` that directly manages the record, or the `has_one` relation for the record that this model is edited on, which is often the parent `DataObject`. -If the `cms_edit_owner` is a `has_one` relation, the class on the other end of the relation _must_ have +If the `cms_edit_owner` is a `has_one` relation, the class on the other end of the relation *must* have a reciprocal `has_many` relation as documented in [Relations](./relations#has-many). For best results, use dot notation on the `has_many` relation. It must also implement a [getCMSEditLinkForManagedDataObject()](api:SilverStripe\Admin\CMSEditLinkExtension::getCMSEditLinkForManagedDataObject()) method. The easiest way to do that is for it to apply the `CMSEditLinkExtension` to the reciprocal class. -** app/src/Model/MyModel.php ** ```php -namespace MyProject\Model; +// app/src/Model/MyModel.php +namespace App\Model; use SilverStripe\Admin\CMSEditLinkExtension; use SilverStripe\ORM\DataObject; @@ -47,11 +47,11 @@ class MyModel extends DataObject } ``` -** app/src/Model/MyParentModel.php ** ```php -namespace MyProject\Model; +// app/src/Model/MyParentModel.php +namespace App\Model; -use MyProject\Admin\MyModelAdmin; +use App\Admin\MyModelAdmin; use SilverStripe\Admin\CMSEditLinkExtension; use SilverStripe\ORM\DataObject; @@ -71,7 +71,7 @@ class MyParentModel extends DataObject [hint] If the `cms_edit_owner` is in some vendor dependency that you don't control, you can always apply `CMSEditLinkExtension` -and the `cms_edit_owner` via yml. +and the `cms_edit_owner` via YAML. [/hint] With the above code examples, you can call `CMSEditLink()` on any instance of `MyModel` or `MyParentModel` and it will produce diff --git a/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md b/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md index 6e4f4dadd..bbab5633a 100644 --- a/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md +++ b/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md @@ -3,33 +3,37 @@ title: Dynamic Default Fields summary: Learn how to add default values to your models --- -# Default Values and Records +# Default values and records [hint] -This page is about defining default values and records in your model class, which only affects _new_ records. You can set defaults directly in the database-schema, which affects _existing_ records as well. See +This page is about defining default values and records in your model class, which only affects *new* records. You can set defaults directly in the database-schema, which affects *existing* records as well. See [Data Types and Casting](/developer_guides/model/data_types_and_casting/#default-values) for details. [/hint] -## Static Default Values +## Static default values + The [DataObject::$defaults](api:SilverStripe\ORM\DataObject::$defaults) array allows you to specify simple static values to be the default values when a record is created. A simple example is if you have a dog and by default its bark is "Woof": + ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Dog extends DataObject +class Dog extends DataObject { private static $db = [ 'Bark' => 'Varchar(10)', ]; - + private static $defaults = [ 'Bark' => 'Woof', ]; } ``` -## Dynamic Default Values +## Dynamic default values In many situations default values need to be dynamically calculated. In order to do this, the [DataObject::populateDefaults()](api:SilverStripe\ORM\DataObject::populateDefaults()) method will need to be overridden. @@ -40,13 +44,22 @@ object! A simple example is to set a field to the current date and time: ```php -/** - * Sets the Date field to the current date. - */ -public function populateDefaults() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class Dog extends DataObject { - $this->Date = date('Y-m-d'); - parent::populateDefaults(); + // ... + + /** + * Sets the Date field to the current date. + */ + public function populateDefaults() + { + $this->Date = date('Y-m-d'); + parent::populateDefaults(); + } } ``` @@ -54,48 +67,61 @@ public function populateDefaults() This method is called very early in the process of instantiating a new record, before any relations are set for it. If you want to set values based on, for example, a `has_one` relation called `Parent`, you can do that by implementing [`onBeforeWrite()`](/developer_guides/model/extending_dataobjects/#onbeforewrite) or a [setter method](/developer_guides/model/data_types_and_casting/#overriding) - for example: ```php -public function onBeforeWrite() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class Dog extends DataObject { - // Only do this if the record hasn't been written to the database yet (optional) - if (!$this->isInDb()) { - $parent = $this->Parent(); - // Set the FullTitle based on the parent, if one exists - if ($parent->exists()) { - $this->FullTitle = $parent->Title . ': ' . $this->Title; - } else { - $this->FullTitle = $this->Title; + // ... + + public function onBeforeWrite() + { + // Only do this if the record hasn't been written to the database yet (optional) + if (!$this->isInDb()) { + $parent = $this->Parent(); + // Set the FullTitle based on the parent, if one exists + if ($parent->exists()) { + $this->FullTitle = $parent->Title . ': ' . $this->Title; + } else { + $this->FullTitle = $this->Title; + } } } -} -// or + // or -public function setFullTitle($value): static -{ - $parent = $this->Parent(); - // Set the FullTitle based on the parent, if one exists - if ($parent->exists()) { - $value = $parent->Title . ': ' . $value; + public function setFullTitle($value): static + { + $parent = $this->Parent(); + // Set the FullTitle based on the parent, if one exists + if ($parent->exists()) { + $value = $parent->Title . ': ' . $value; + } + return $this->setField('FullTitle', $value); } - return $this->setField('FullTitle', $value); } ``` [/hint] -## Static Default Records +## Static default records + The [DataObject::$default_records](api:SilverStripe\ORM\DataObject::$default_records) array allows you to specify default records created on dev/build. A simple example of this is having a region model and wanting a list of regions created when the site is built: + ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Region extends DataObject +class Region extends DataObject { private static $db = [ 'Title' => 'Varchar(45)', ]; - + private static $default_records = [ ['Title' => 'Auckland'], ['Title' => 'Coromandel'], @@ -104,9 +130,9 @@ class Region extends DataObject } ``` -## Dynamic Default Records +## Dynamic default records -Just like default values, there are times when you want your default _records_ to have some dynamic value or to be created only under certain conditions. To achive this, override the +Just like default values, there are times when you want your default *records* to have some dynamic value or to be created only under certain conditions. To achive this, override the [DataObject::requireDefaultRecords()](api:SilverStripe\ORM\DataObject::requireDefaultRecords()) method. ```php @@ -116,7 +142,8 @@ use SilverStripe\Control\Director; public function requireDefaultRecords() { - // Require the base defaults first - that way the records we create below won't interfere with any declared in $default_records + // Require the base defaults first - that way the records we create below won't interfere with any + // declared in $default_records parent::requireDefaultRecords(); // Make some record only if we're in dev mode and we don't have any of the current class yet. diff --git a/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md b/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md index 75b10904f..3ebb4d249 100644 --- a/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md +++ b/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md @@ -11,68 +11,75 @@ These lists can get quite long, and hard to present on a single list. by splitting up the list into multiple pages. In this howto, we present an alternative to pagination: -grouping a list by various criteria, through the [GroupedList](api:SilverStripe\ORM\GroupedList) class. -This class is a [ListDecorator](api:SilverStripe\ORM\ListDecorator), which means it wraps around a list, +grouping a list by various criteria, through the [`GroupedList`](api:SilverStripe\ORM\GroupedList) class. +This class is a [`ListDecorator`](api:SilverStripe\ORM\ListDecorator), which means it wraps around a list, adding new functionality. -It provides a `groupBy()` method, which takes a field name, and breaks up the managed list -into a number of arrays, where each array contains only objects with the same value of that field. +It provides a `groupBy()` method, which takes a field name, and breaks up the managed list +into a number of arrays, where each array contains only objects with the same value of that field. Similarly, the `GroupedBy()` method builds on this and returns the same data in a template-friendly format. -## Grouping Sets By First Letter +## Grouping sets by first letter -This example deals with breaking up a [SS_List](api:SilverStripe\ORM\SS_List) into sub-headings by the first letter. +This example deals with breaking up a [`SS_List`](api:SilverStripe\ORM\SS_List) into sub-headings by the first letter. Let's say you have a set of Module objects, each representing a Silverstripe CMS module, and you want to output a list of these in alphabetical order, with each letter as a heading; something like the following list: - * B - * Blog - * C - * CMS Workflow - * Custom Translations - * D - * Database Plumber - * ... +```text +* B + * Blog +* C + * CMS Workflow + * Custom Translations +* D + * Database Plumber + * ... +``` -The first step is to set up the basic data model, +The first step is to set up the basic data model, along with a method that returns the first letter of the title. This will be used both for grouping and for the title in the template. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Module extends DataObject +class Module extends DataObject { private static $db = [ - 'Title' => 'Text' + 'Title' => 'Text', ]; /** * Returns the first letter of the module title, used for grouping. * @return string */ - public function getTitleFirstLetter() + public function getTitleFirstLetter() { return $this->Title[0]; } } ``` -The next step is to create a method or variable that will contain/return all the objects, -sorted by title. For this example this will be a method on the `Page` class. +The next step is to create a method or variable that will contain/return all the objects, +sorted by title. For this example this will be a method on a new `ModulePage` class. ```php -use SilverStripe\CMS\Model\SiteTree; +namespace App\PageType; + +use App\Model\Module; +use Page; use SilverStripe\ORM\GroupedList; -class Page extends SiteTree +class ModulePage extends Page { /** * Returns all modules, sorted by their title. * @return GroupedList */ - public function getGroupedModules() + public function getGroupedModules() { return GroupedList::create(Module::get()->sort('Title')); } @@ -84,7 +91,7 @@ Notice that we're sorting as part of the ORM call. While `GroupedList` does have [/notice] The final step is to render this into a template. The `GroupedBy()` method breaks up the set into -a number of sets, grouped by the field that is passed as the parameter. +a number of sets, grouped by the field that is passed as the parameter. In this case, the `getTitleFirstLetter()` method defined earlier is used to break them up. ```ss @@ -100,11 +107,11 @@ In this case, the `getTitleFirstLetter()` method defined earlier is used to brea <% end_loop %> ``` -## Grouping Sets By Month +## Grouping sets by month -Grouping a set by month is a very similar process. +Grouping a set by month is a very similar process. The only difference would be to sort the records by month name, and -then create a method on the DataObject that returns the month name, +then create a method on the DataObject that returns the month name, and pass that to the [GroupedList::GroupedBy()](api:SilverStripe\ORM\GroupedList::GroupedBy()) call. We're reusing our example `Module` object, @@ -113,35 +120,40 @@ which is automatically set when the record is first written to the database. This will have a method which returns the month it was posted in: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Module extends DataObject +class Module extends DataObject { /** * Returns the month name this news item was posted in. * @return string */ - public function getMonthCreated() + public function getMonthCreated() { return date('F', strtotime($this->Created)); } } ``` -The next step is to create a method that will return all records that exist, +The next step is to create a method that will return all records that exist, sorted by month name from January to December. This can be accomplshed by sorting by the `Created` field: ```php -use SilverStripe\CMS\Model\SiteTree; +namespace App\PageType; + +use App\Model\Module; +use Page; use SilverStripe\ORM\GroupedList; -class Page extends SiteTree +class ModulePage extends Page { /** * Returns all news items, sorted by the month they were posted * @return GroupedList */ - public function getGroupedModulesByDate() + public function getGroupedModulesByDate() { return GroupedList::create(Module::get()->sort('Created')); } @@ -165,4 +177,4 @@ The final step is to render this into the template using the [GroupedList::Group ## Related - * [Howto: "Pagination"](/developer_guides/templates/how_tos/pagination) +- [Howto: "Pagination"](/developer_guides/templates/how_tos/pagination) diff --git a/en/02_Developer_Guides/00_Model/How_Tos/index.md b/en/02_Developer_Guides/00_Model/How_Tos/index.md index 29eb663e5..d76e261ea 100644 --- a/en/02_Developer_Guides/00_Model/How_Tos/index.md +++ b/en/02_Developer_Guides/00_Model/How_Tos/index.md @@ -1,6 +1,6 @@ --- title: How To's --- -# How To's: Model and Databases +# How to's: model and databases -[CHILDREN] \ No newline at end of file +[CHILDREN] diff --git a/en/02_Developer_Guides/00_Model/index.md b/en/02_Developer_Guides/00_Model/index.md index 62306d9da..ac9db226b 100644 --- a/en/02_Developer_Guides/00_Model/index.md +++ b/en/02_Developer_Guides/00_Model/index.md @@ -5,8 +5,10 @@ introduction: This guide will cover how to create and manipulate data within Sil icon: database --- +# Model and databases + In Silverstripe CMS, application data is typically represented by [`DataObject`](api:SilverStripe\ORM\DataObject) models. A `DataObject` subclass defines the -data columns, relationships and properties of a particular data record. For example, [`Member`](api:SilverStripe\Security\Member) is a `DataObject` +data columns, relationships and properties of a particular data record. For example, [`Member`](api:SilverStripe\Security\Member) is a `DataObject` which stores information about a person who has authenticated access to your project. [CHILDREN Exclude="How_tos"] diff --git a/en/02_Developer_Guides/01_Templates/01_Syntax.md b/en/02_Developer_Guides/01_Templates/01_Syntax.md index e24c3af40..64a16b83f 100644 --- a/en/02_Developer_Guides/01_Templates/01_Syntax.md +++ b/en/02_Developer_Guides/01_Templates/01_Syntax.md @@ -4,7 +4,7 @@ summary: A look at the operations, variables and language controls you can use w icon: code --- -# Template Syntax +# Template syntax A template can contain any markup language (e.g HTML, CSV, JSON, etc) and before being rendered to the user, they're processed through [SSViewer](api:SilverStripe\View\SSViewer). This process replaces placeholders such as `$Var` with real content from your @@ -12,9 +12,8 @@ model (see [Model and Databases](../model)) and allows you to use logic like `<% An example of a Silverstripe CMS template is below: -**app/templates/Page.ss** - ```ss +<%-- app/templates/Page.ss --%> <% base_tag %> @@ -45,7 +44,7 @@ An example of a Silverstripe CMS template is below: ``` [note] -Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other +Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other text-based format. [/note] @@ -77,17 +76,17 @@ $Foo.Bar These variables will call a method / field on the object and insert the returned value as a string into the template. -* `$Foo("param")` will call `$obj->Foo("param")` -* `$Foo.Bar` will call `$obj->Foo()->Bar` +- `$Foo("param")` will call `$obj->Foo("param")` +- `$Foo.Bar` will call `$obj->Foo()->Bar` [info] Arguments passed into methods can be any non-array literal type (not just strings), e.g: -* `$Foo(1)` will pass `1` as an int -* `$Foo(0.5)` will pass `0.5` as a float -* `$Foo(true)` will pass `true` as a boolean -* `$Foo(null)` will pass `null` as a null primitive -* `$Foo("param")`, `$Foo('param')`, and `$Foo(param)` will all pass `'param'` as a string. It is recommended that you always use quotes when passing a string for clarity +- `$Foo(1)` will pass `1` as an int +- `$Foo(0.5)` will pass `0.5` as a float +- `$Foo(true)` will pass `true` as a boolean +- `$Foo(null)` will pass `null` as a null primitive +- `$Foo("param")`, `$Foo('param')`, and `$Foo(param)` will all pass `'param'` as a string. It is recommended that you always use quotes when passing a string for clarity [/info] [notice] @@ -102,23 +101,30 @@ been defined, the system will return an error. [note] For more details around how variables are inserted and formatted into a template see -[Formatting, Modifying and Casting Variables](casting) +[Formatting, Modifying and Casting Variables](/developer_guides/templates/casting/) [/note] Variables can come from your database fields, or custom methods you define on your objects. -**app/src/Page.php** - ```php -public function UsersIpAddress() +// app/src/Model/MyObject.php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyObject extends DataObject { - return $this->getRequest()->getIP(); + // ... + + public function UsersIpAddress() + { + return $this->getRequest()->getIP(); + } } ``` -**app/src/Page.ss** - ```html +

You are coming from $UsersIpAddress.

``` @@ -130,9 +136,9 @@ The variables that can be used in a template vary based on the object currently object the methods get called on. For the standard `Page.ss` template the scope is the current [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) object. This object provides access to all the public methods on that controller, as well as the public methods, relations, and database fields for its corresponding [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) record. -**app/src/Layout/Page.ss** - ```ss +<%-- app/templates/Layout/Page.ss --%> + <%-- calls `SilverStripeNavigator()` on the controller and prints markup for the `SilverStripeNavigator` for this page --%> $SilverStripeNavigator @@ -140,7 +146,7 @@ $SilverStripeNavigator $Content ``` -## Conditional Logic +## Conditional logic The simplest conditional block is to check for the presence of a value. This effectively works the same as [`isset()`](https://www.php.net/manual/en/function.isset.php) in PHP - i.e. if there is no variable available with that name, or the variable's value is `0`, `false`, or `null`, the condition will be false. @@ -204,7 +210,7 @@ For more nuanced conditions you can use the `!=` operator. <% end_if %> ``` -### Boolean Logic +### Boolean logic Multiple checks can be done using `||`/`or`, or `&&`/ `and`. @@ -249,7 +255,7 @@ an additional `Includes` directory will be inserted into the resolved path just <% include MyNamespace/SideBar %> ``` -The `include` tag can be particularly helpful for nested functionality and breaking large templates up. In this example, +The `include` tag can be particularly helpful for nested functionality and breaking large templates up. In this example, the include only happens if the user is logged in. ```ss @@ -258,7 +264,7 @@ the include only happens if the user is logged in. <% end_if %> ``` -Includes can't directly access the parent scope when the include is included. However you can pass arguments to the +Includes can't directly access the parent scope when the include is included. However you can pass arguments to the include. ```ss @@ -268,12 +274,12 @@ include. ``` [hint] -Unlike when passing arguments to a function call in templates, arguments passed to a template include can be literals _or_ variables. +Unlike when passing arguments to a function call in templates, arguments passed to a template include can be literals *or* variables. [/hint] -## Looping Over Lists +## Looping over lists -The `<% loop %>` tag is used to iterate or loop over a collection of items such as [DataList](api:SilverStripe\ORM\DataList) or an [ArrayList](api:SilverStripe\ORM\ArrayList) +The `<% loop %>` tag is used to iterate or loop over a collection of items such as [DataList](api:SilverStripe\ORM\DataList) or an [ArrayList](api:SilverStripe\ORM\ArrayList) collection. ```ss @@ -285,12 +291,12 @@ collection. ``` -This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each -page. +This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each +page. [notice] The `$Title` inside the loop refers to the Title property on each object that is looped over, not the current page like -the reference of `$Title` outside the loop. +the reference of `$Title` outside the loop. This demonstrates the concept of scope ([see scope below](#scope)). When inside a `<% loop %>` the scope of the template has changed to the object that is being looped over. @@ -351,21 +357,21 @@ Methods can also be chained. ``` -### Position Indicators +### Position indicators -Inside the loop scope, there are many variables at your disposal to determine the current position in the list and +Inside the loop scope, there are many variables at your disposal to determine the current position in the list and iteration. These are provided by [`SSViewer_BasicIteratorSupport::get_template_iterator_variables()`](api:SilverStripe\View\SSViewer_BasicIteratorSupport::get_template_iterator_variables()). - * `$Even`, `$Odd`: Returns boolean based on the current position in the list (see `$Pos` below). Handy for zebra striping. - * `$EvenOdd`: Returns a string based on the current position in the list, either 'even' or 'odd'. Useful for CSS classes. - * `$IsFirst`, `$IsLast`, `$Middle`: Booleans about the position in the list. All items that are not first or last are considered to be in the middle. - * `$FirstLast`: Returns a string, "first", "last", "first last" (if both), or "" (if middle). Useful for CSS classes. - * `$MiddleString`: Returns a string, "middle" if the item is in the middle, or "" otherwise. - * `$Pos`: The current position in the list (integer). +- `$Even`, `$Odd`: Returns boolean based on the current position in the list (see `$Pos` below). Handy for zebra striping. +- `$EvenOdd`: Returns a string based on the current position in the list, either 'even' or 'odd'. Useful for CSS classes. +- `$IsFirst`, `$IsLast`, `$Middle`: Booleans about the position in the list. All items that are not first or last are considered to be in the middle. +- `$FirstLast`: Returns a string, "first", "last", "first last" (if both), or "" (if middle). Useful for CSS classes. +- `$MiddleString`: Returns a string, "middle" if the item is in the middle, or "" otherwise. +- `$Pos`: The current position in the list (integer). Will start at 1, but can take a starting index as a parameter. - * `$FromEnd`: The position of the item from the end (integer). +- `$FromEnd`: The position of the item from the end (integer). Last item defaults to 1, but can be passed as a parameter. - * `$TotalItems`: Number of items in the list (integer). +- `$TotalItems`: Number of items in the list (integer). ```ss
    @@ -380,18 +386,18 @@ iteration. These are provided by [`SSViewer_BasicIteratorSupport::get_template_i ``` [info] -A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding +A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding pagination. [/info] -### Modulus and MultipleOf +### `Modulus` and `MultipleOf` `$Modulus` and `$MultipleOf` can help to build column and grid layouts. `$Modulus` returns the modulus of the numerical position of the item in the data set. You must pass in the number to perform modulus operations to and an optional offset to start from. It returns an integer. [hint] -`$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put `$Modulus(3)` as a class and add a +`$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put `column-$Modulus(3)` as a class and add a `clear: both` to `.column-1`. [/hint] @@ -448,22 +454,24 @@ For more information on formatting and casting variables see [Formatting, Modify ## Scope -In the `<% loop %>` section, we saw an example of two **scopes**. Outside the `<% loop %>...<% end_loop %>`, we were in +In the `<% loop %>` section, we saw an example of two **scopes**. Outside the `<% loop %>...<% end_loop %>`, we were in the scope of the top level `Page`. But inside the loop, we were in the scope of an item in the list (i.e.the `Child`). -The scope determines where the value comes from when you refer to a variable. Typically the outer scope of a `Page.ss` -layout template is the [PageController](api:SilverStripe\CMS\Controllers\ContentController\PageController) that is currently being rendered. +The scope determines where the value comes from when you refer to a variable. Typically the outer scope of a `Page.ss` +layout template is the [PageController](api:SilverStripe\CMS\Controllers\ContentController\PageController) that is currently being rendered. When the scope is a `PageController` it will automatically also look up any methods in the corresponding `Page` data record. In the case of `$Title` the flow looks like - $Title --> [Looks up: Current PageController and parent classes] --> [Looks up: Current Page and parent classes] +```text +$Title --> [Looks up: Current PageController and parent classes] --> [Looks up: Current Page and parent classes] +``` The list of variables you could use in your template is the total of all the methods in the current scope object, parent classes of the current scope object, any failovers for the current scope object (e.g. controllers and pages can access each others' methods/properties), and any [`Extension`](api:SilverStripe\Core\Extension) instances you have applied to any of those. -### Navigating Scope +### Navigating scope #### Up @@ -483,14 +491,14 @@ When in a particular scope, `$Up` takes the scope back to the previous level. Given the following structure: -``` - My Page - | - +-+ Child 1 - | | - | +- Grandchild 1 - | - +-+ Child 2 +```text +My Page +| ++-+ Child 1 +| | +| +- Grandchild 1 +| ++-+ Child 2 ``` It will create this markup: @@ -518,7 +526,7 @@ Each `<% loop %>` or `<% with %>` block results in a change of scope, regardless #### Top -While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the +While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the template. The previous example could be rewritten to use the following syntax. ```ss @@ -552,7 +560,7 @@ Hello, $CurrentMember.FirstName, welcome back. Your current balance is $CurrentM Notice that the first example is much tidier, as it removes the repeated use of the `$CurrentMember` accessor. Outside the `<% with %>`, we are in the page scope. Inside it, we are in the scope of `$CurrentMember` object. We can -refer directly to properties and methods of the [Member](api:SilverStripe\Security\Member) object. `$FirstName` inside the scope is equivalent to +refer directly to properties and methods of the [`Member`](api:SilverStripe\Security\Member) object. `$FirstName` inside the scope is equivalent to `$CurrentMember.FirstName`. ### `fortemplate()` and `$Me` {#fortemplate} @@ -560,14 +568,15 @@ refer directly to properties and methods of the [Member](api:SilverStripe\Securi If you reference some `ViewableData` object directly in a template, the `forTemplate()` method on that object will be called. This can be used to provide a default template for an object. -**app/src/Page.php** - ```php -use SilverStripe\CMS\Model\SiteTree; +// app/src/PageType/HomePage.php +namespace App\PageType; -class Page extends SiteTree +use Page; + +class HomePage extends Page { - public function forTemplate() + public function forTemplate() { // We can also render a template here using $this->renderWith() return 'Page: ' . $this->Title; @@ -582,9 +591,9 @@ $Pages->First You can also use the `$Me` variable, which outputs the current object in scope by calling `forTemplate()` on the object. -**app/templates/Page.ss** - ```ss +<%-- app/templates/App/PageType/Layout/HomePage.ss --%> + <%-- calls forTemplate() on the current object in scope and prints Page: Home --%> $Me ``` @@ -608,10 +617,11 @@ for adding notes for other developers but for things you don't want published in $EditForm <%-- Some hidden comment about the form --%> ``` -## Related Lessons -* [Creating your first theme](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-theme-1) +## Related lessons + +- [Creating your first theme](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-theme-1) -## Related Documentation +## Related documentation [CHILDREN Exclude="How_Tos"] @@ -619,9 +629,7 @@ $EditForm <%-- Some hidden comment about the form --%> [CHILDREN Folder="How_Tos"] -## API Documentation - -* [SSViewer](api:SilverStripe\View\SSViewer) -* [ThemeManifest](api:SilverStripe\View\ThemeManifest) - +## API documentation +- [SSViewer](api:SilverStripe\View\SSViewer) +- [ThemeManifest](api:SilverStripe\View\ThemeManifest) diff --git a/en/02_Developer_Guides/01_Templates/02_Common_Variables.md b/en/02_Developer_Guides/01_Templates/02_Common_Variables.md index 6663510b4..27c28775f 100644 --- a/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -3,7 +3,7 @@ title: Common Variables summary: Some of the common variables and methods your templates can use, including Menu, SiteConfig, and more. --- -# Common Variables +# Common variables The page below describes a few of common variables and methods you'll see in a Silverstripe CMS template. This is not an exhaustive list. From your template you can call any method, database field, or relation on the object which is @@ -29,7 +29,7 @@ Some of the following only apply when you have the `silverstripe/cms` module ins functionality may not be included. [/alert] -## Base Tag +## Base tag ```ss @@ -40,7 +40,7 @@ functionality may not be included. The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as ``) will become relative to the URI specified in the base tag. This ensures the -browser knows where to locate your site’s images and css files. +browser knows where to locate your site’s images and CSS files. It renders in the template as `` @@ -48,7 +48,7 @@ It renders in the template as `
    @@ -234,7 +229,7 @@ you need to be careful of nesting to ensure you don't break the output. ### Location -Element scoped shortcodes have a special ability to move the location they are inserted at to comply with HTML lexical +Element scoped shortcodes have a special ability to move the location they are inserted at to comply with HTML lexical rules. Take for example this basic paragraph tag: ```html @@ -267,13 +262,24 @@ The result is this: Here is a summary of the callback parameter values based on some example shortcodes. ```php -public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName) +namespace App\ShortCode; + +use SilverStripe\View\Parsers\ShortcodeParser; + +class MyShortCodeProvider { - // .. + public static function myCustomShortCode( + array $arguments, + ?string $content, + ShortcodeParser $parser, + string $tagName + ) { + // ... + } } ``` -``` +```text [my_shortcode] $attributes => []; $content => null; @@ -281,7 +287,7 @@ $parser => ShortcodeParser instance, $tagName => 'my_shortcode' ``` -``` +```text [my_shortcode,attribute="foo",other="bar"] $attributes => ['attribute' => 'foo', 'other' => 'bar'] $enclosedContent => null @@ -289,7 +295,7 @@ $parser => ShortcodeParser instance $tagName => 'my_shortcode' ``` -``` +```text [my_shortcode,attribute="foo"]content[/my_shortcode] $attributes => ['attribute' => 'foo'] $enclosedContent => 'content' @@ -310,11 +316,11 @@ example the below code will not work as expected: The parser will raise an error if it can not find a matching opening tag for any particular closing tag -## Related Documentation +## Related documentation - * [Wordpress Implementation](https://codex.wordpress.org/Shortcode_API) - * [How to Create a Google Maps Shortcode](how_tos/create_a_google_maps_shortcode) +- [Wordpress Implementation](https://codex.wordpress.org/Shortcode_API) +- [How to Create a Google Maps Shortcode](how_tos/create_a_google_maps_shortcode) -## API Documentation +## API documentation - * [ShortcodeParser](api:SilverStripe\View\Parsers\ShortcodeParser) +- [ShortcodeParser](api:SilverStripe\View\Parsers\ShortcodeParser) diff --git a/en/02_Developer_Guides/05_Extending/05_Injector.md b/en/02_Developer_Guides/05_Extending/05_Injector.md index a4516d131..7f12ad1b5 100644 --- a/en/02_Developer_Guides/05_Extending/05_Injector.md +++ b/en/02_Developer_Guides/05_Extending/05_Injector.md @@ -4,32 +4,31 @@ summary: Introduction to using Dependency Injection within Silverstripe CMS. icon: code --- -# Dependency Injection +# Dependency injection > ...dependency injection is a design pattern in which an object or function receives other objects or functions that it depends on [Wikipedia](https://en.wikipedia.org/wiki/Dependency_injection) -In Silverstripe a combination of the [Injector API](#Injector) and the [Configuration API](../configuration) provide a comprehensive dependency injection pattern. +In Silverstripe a combination of the [Injector API](#injector) and the [Configuration API](../configuration) provide a comprehensive dependency injection pattern. Some of the goals of dependency injection are: -* Simplified instantiation of objects -* Providing a uniform way of declaring and managing inter-object dependencies -* Promoting abstraction of logic +- Simplified instantiation of objects +- Providing a uniform way of declaring and managing inter-object dependencies +- Promoting abstraction of logic -In practical terms it allows developers to: +In practical terms it allows developers to: -* Make class dependencies configurable rather than hard-coded -* Override or replace core behaviour without needing to alter core code -* Write more testable code +- Make class dependencies configurable rather than hard-coded +- Override or replace core behaviour without needing to alter core code +- Write more testable code +## `Injector` -# Injector +The [Injector](api:SilverStripe\Core\Injector\Injector) class is the central manager of inter-class dependencies in Silverstripe CMS. It offers developers the +ability to declare the dependencies a class type has, or to change the nature of the dependencies defined by other +developers. -The [Injector](api:SilverStripe\Core\Injector\Injector) class is the central manager of inter-class dependencies in Silverstripe CMS. It offers developers the -ability to declare the dependencies a class type has, or to change the nature of the dependencies defined by other -developers. - -## Basic usage +### Basic usage The following snippet shows `Injector` creating a new object of type `App\MyClient` through its `create` method: @@ -64,7 +63,7 @@ $object = Injector::inst()->create(MyClient::class, $arg1, $arg2); Note that for classes that use the [`Injectable`](api:SilverStripe\Core\Injector\Injectable) trait, there is a simpler syntax for this and for the singleton pattern mentioned below. See [Injectable Trait](#injectable-trait) below for details. -### Singleton Pattern +### Singleton pattern The `Injector` API can be used for the [singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) through `get()`. Unlike `create()` subsequent calls to `get` return the same object instance as the first call. @@ -99,7 +98,7 @@ $object = Injector::inst()->get(MyClient::class, constructorArgs: [$arg1, $arg2] It is possible to tell `Injector` to always instantiate a new object for a given service even if it's requested it as a singleton. This is particularly useful when a service is intended to be [declared as a dependency](#dependencies-and-properties) for some other class, and you don't want that dependency to be a singleton. -This is done by setting the 'type' for a given service definition to "prototype" in yaml configuration like so: +This is done by setting the 'type' for a given service definition to "prototype" in YAML configuration like so: ```yml SilverStripe\Core\Injector\Injector: @@ -144,13 +143,12 @@ Note that `App\MyClient` [does not have to be an existing class](#service-inheri Using Injector imperatively like this is most common [in testing](#testing-with-injector). Usually, the configuration API is used instead. -## Injector API 🤝 Configuration API {#injector-via-config} +## Injector API 🤝 configuration API {#injector-via-config} The Injector API combined with the Configuration API is a powerful way to declare and manage dependencies in your code. For example, `App\MyClient` can be swapped out using the following config: -**app/_config/app.yml** - ```yml +# app/_config/class-overrides.yml SilverStripe\Core\Injector\Injector: App\MyClient: class: App\MyBetterClient @@ -172,7 +170,7 @@ This allows you to concisely override classes in Silverstripe core or other thir When overriding other configuration beware the [order that configuration is applied](../configuration/#configuration-values). You may have to use the [Before/After](../configuration/#before-after-rules) syntax to apply your override. [/info] -### Special YML Syntax +### Special YAML syntax You can use the special `%$` prefix in the injector configuration yml to fetch items via the Injector. For example: @@ -199,7 +197,7 @@ SilverStripe\Core\Injector\Injector: The Injector configuration has the special ability to include core constants or environment variables. They can be used by quoting with back ticks "`". Please ensure you also quote the entire value (see below). -```yaml +```yml SilverStripe\Core\Injector\Injector: CachingService: class: SilverStripe\Cache\CacheProvider @@ -235,26 +233,27 @@ SilverStripe\Core\Injector\Injector: ThisWillNotSubstitute: 'lorem `REGULAR_TEXT` ipsum' ``` -## Dependencies and Properties +## Dependencies and properties -Silverstripe classes can declare a special `$dependencies` array which can quickly configure dependencies when used with the injector API. The `Injector` will evaluate the array values and assign the appropriate value to a property that matches the array key. For example: +Silverstripe classes can declare a special `$dependencies` array which can quickly configure dependencies when used with the injector API. The `Injector` will evaluate the array values and assign the appropriate value to a property that matches the array key. For example: [info] -Just like the yaml syntax discussed above, constants and environment variables can be substitutes in dependency values using backticks. +Just like the YAML syntax discussed above, constants and environment variables can be substitutes in dependency values using backticks. [/info] ```php -namespace App; +namespace App\Control; use SilverStripe\Control\Controller; use ThirdParty\PermissionService; -class MyController extends Controller +class MyController extends Controller { /** * Properties matching the array keys in $dependencies will be automatically * set by the injector on object creation. */ + // phpcs:ignore SlevomatCodingStandard.Classes.ForbiddenPublicProperty.ForbiddenPublicProperty public $textProperty; /** @@ -285,15 +284,16 @@ class MyController extends Controller ``` [info] -Note the properties set by Injector must be public properties, or have a public setter method. +Note the properties set by `Injector` must be public properties, or have a public setter method. [/info] -When creating a new instance of `App\MyController` via Injector the permissions property will contain an instance of the `ThirdParty\PermissionService` that was resolved by Injector, and the `defaultText` property will contain the string defined in the `$dependencies` array. +When creating a new instance of `App\Control\MyController` via Injector the permissions property will contain an instance of the `ThirdParty\PermissionService` that was resolved by Injector, and the `defaultText` property will contain the string defined in the `$dependencies` array. ```php +use App\Control\MyController; use SilverStripe\Core\Injector\Injector; -$object = Injector::inst()->get(App\MyController::class); +$object = Injector::inst()->get(MyController::class); // prints 'ThirdParty\PermissionService' echo get_class($object->permissions); @@ -304,13 +304,12 @@ echo $object->getDefaultText(); We can then change or override any of those dependencies via the [Configuration YAML](../configuration) and Injector does the hard work of wiring it up. -**app/_config/app.yml** - ```yml +# app/_config/services.yml SilverStripe\Core\Injector\Injector: ThirdParty\PermissionService: class: App\MyCustomPermissionService - App\MyController: + App\Control\MyController: properties: defaultText: 'Replaces the old text' ``` @@ -318,9 +317,10 @@ SilverStripe\Core\Injector\Injector: Now the dependencies will be replaced with our configuration. ```php +use App\Control\MyController; use SilverStripe\Core\Injector\Injector; -$object = Injector::inst()->get(App\MyController::class); +$object = Injector::inst()->get(MyController::class); // prints 'App\MyCustomPermissionService' echo get_class($object->permissions); @@ -329,9 +329,9 @@ echo get_class($object->permissions); echo $object->getDefaultText(); ``` -### Dependent Calls +### Dependent calls -As well as properties, method calls the class depends on (i.e. method calls that should be done after instantiating the object) can also be specified via the `calls` property in yaml: +As well as properties, method calls the class depends on (i.e. method calls that should be done after instantiating the object) can also be specified via the `calls` property in YAML: ```yml SilverStripe\Core\Injector\Injector: @@ -347,36 +347,51 @@ Note that [configuration is merged](../configuration/#configuration-values) so t ### Managed objects -While dependencies can be specified in PHP with the `$dependencies` configuration property, it is common to define them in yaml - especially when there is a chain of dependencies and other related configuration which needs to be defined. +While dependencies can be specified in PHP with the `$dependencies` configuration property, it is common to define them in YAML - especially when there is a chain of dependencies and other related configuration which needs to be defined. For example. assuming a class structure such as this: ```php -namespace App; +namespace App\Control; -class MyController +class MyController { - public $permissions; + private $permissions; + + private static $dependencies = []; - private static $dependencies = []; + public function setPermissions($permissions): static + { + $this->permissions = $permissions; + return $this; + } } +``` -class RestrictivePermissionService +```php +namespace App\Control; + +class RestrictivePermissionService { private $database; - public function setDatabase($d) - { - $this->database = $d; + public function setDatabase($db): static + { + $this->database = $db; } } +``` -class MySQLDatabase +```php +namespace App\ORM; + +class MySQLDatabase { private $username; + private $password; - - public function __construct($username, $password) + + public function __construct($username, $password) { $this->username = $username; $this->password = $password; @@ -390,15 +405,15 @@ And the following configuration.. --- name: MyController --- -App\MyController: +App\Control\MyController: dependencies: - permissions: '%$App\PermissionService' + permissions: '%$PermissionService' SilverStripe\Core\Injector\Injector: - App\PermissionService: - class: App\RestrictivePermissionService + PermissionService: + class: App\Control\RestrictivePermissionService properties: - database: '%$App\MySQLDatabase' - App\MySQLDatabase: + database: '%$App\ORM\MySQLDatabase' + App\ORM\MySQLDatabase: constructor: 0: '`dbusername`' 1: '`dbpassword`' @@ -407,7 +422,7 @@ SilverStripe\Core\Injector\Injector: Calling.. ```php -use App\MyController; +use App\Control\MyController; use SilverStripe\Core\Injector\Injector; $controller = Injector::inst()->get(MyController::class); @@ -415,19 +430,19 @@ $controller = Injector::inst()->get(MyController::class); Would perform the following steps: -* Fetches a singleton of the `App\MyController` service -* If there no existing instance for the `App\MyController` singleton: - * Instantiates the service as an instance of the `App\MyController` class - * Look through the `dependencies` for the `App\MyController` service and fetch a singleton of the `App\PermissionService` service - * If there no existing instance for the `App\PermissionService` singleton: - * Instantiates the service as an instance of the `App\RestrictivePermissionService` class - * Look at the properties to be injected for the `App\PermissionService` service fetch a singleton of the `App\MySQLDatabase` service - * If there no existing instance for the `App\MySQLDatabase` singleton: - * Instantiates the service as an instance of the `App\MySQLDatabase` class - * Evaluates and passes in the values of the constants or environment variables `dbusername` and `dbpassword` as arguments to the constructor - * Sets the `App\MySQLDatabase` singleton as the private `database` property on the `App\PermissionService` singleton by passing it in to a call to the `setDatabase()` method - * Sets the `App\PermissionService` singleton as the public `permissions` property on the `App\MyController` singleton -* Returns the `App\MyController` singleton +- Fetches a singleton of the `App\Control\MyController` service +- If there no existing instance for the `App\Control\MyController` singleton: + - Instantiates the service as an instance of the `App\Control\MyController` class + - Look through the `dependencies` for the `App\Control\MyController` service and fetch a singleton of the `App\PermissionService` service + - If there no existing instance for the `App\PermissionService` singleton: + - Instantiates the service as an instance of the `App\RestrictivePermissionService` class + - Look at the properties to be injected for the `App\PermissionService` service fetch a singleton of the `App\ORM\MySQLDatabase` service + - If there no existing instance for the `App\ORM\MySQLDatabase` singleton: + - Instantiates the service as an instance of the `App\ORM\MySQLDatabase` class + - Evaluates and passes in the values of the constants or environment variables `dbusername` and `dbpassword` as arguments to the constructor + - Sets the `App\ORM\MySQLDatabase` singleton as the private `database` property on the `App\PermissionService` singleton by passing it in to a call to the `setDatabase()` method + - Sets the `App\PermissionService` singleton as the public `permissions` property on the `App\Control\MyController` singleton +- Returns the `App\Control\MyController` singleton ## Factories @@ -442,24 +457,22 @@ and the factory service will be used. An example using the `App\MyFactory` service to create instances of the `App\MyService` service is shown below: -**app/_config/app.yml** - ```yml +# app/_config/services.yml SilverStripe\Core\Injector\Injector: App\MyService: factory: App\MyFactory ``` -**app/src/MyFactory.php** - ```php +// app/src/MyFactory.php namespace App; use SilverStripe\Core\Injector\Factory; class MyFactory implements Factory { - public function create($service, array $params = []) + public function create($service, array $params = []) { return new MyServiceImplementation(...$params); } @@ -490,9 +503,8 @@ The method can be (but does not have to be) a static method. An example of HTTP Client service with extra logging middleware which uses factories to be instantiated: -**app/_config/app.yml** - ```yml +# app/_config/services.yml SilverStripe\Core\Injector\Injector: App\LogMiddleware: factory: 'GuzzleHttp\Middleware' @@ -518,10 +530,10 @@ By default, services registered with Injector do not inherit from one another; T named services, which may not be actual classes, and thus should not behave as though they were. Thus if you want an object to have the injected dependencies of a service of another name, you must -assign a reference to that service. References are denoted by using a percent and dollar sign, like in the +assign a reference to that service. References are denoted by using a percent and dollar sign, like in the YAML configuration example below. -```yaml +```yml SilverStripe\Core\Injector\Injector: App\JSONServiceDefinition: class: App\JSONServiceImplementor @@ -550,17 +562,16 @@ instances will be classes which match their respective service names (i.e. `App\ and `App\ServiceConnector` will be an instance of `App\ServiceConnector`), due to the lack of a `class` specification on either service definition. -## Testing with Injector +## Testing with injector In situations where service definitions must be temporarily overridden, it is possible to create nested Injector instances which may be later discarded, reverting the application to the original state. This is done through `nest` and `unnest`. This is useful when writing test cases, as certain services may be necessary to override for a single method call. - ```php -use App\MyService; use App\LiveService; +use App\MyService; use App\TestingService; use SilverStripe\Core\Injector\Injector; @@ -578,7 +589,7 @@ $service = Injector::inst()->get(MyService::class); Injector::unnest(); ``` -## Injectable Trait +## Injectable trait The [`Injectable`](api:SilverStripe\Core\Injector\Injectable) trait can be used to indicate your class is able to be used with Injector (though it is not required). It provides the `create` and `singleton` methods to shortcut creating objects through Injector. @@ -586,10 +597,12 @@ For example with the following class: ```php namespace App; + use SilverStripe\Core\Injector\Injectable; -class MyClass { - use Injectable; +class MyClass +{ + use Injectable; } ``` @@ -618,7 +631,7 @@ $singletonObject = Injector::inst()->get(MyClass::class); this might look familar as it is the standard way to instantiate a `DataObject` (e.g `Page::create()`) and many other objects in Silverstripe CMS. Using this syntax rather than `new Page()` allows the object to be overridden by dependency injection. -## API Documentation +## API documentation -* [Injector](api:SilverStripe\Core\Injector\Injector) -* [Factory](api:SilverStripe\Core\Injector\Factory) +- [Injector](api:SilverStripe\Core\Injector\Injector) +- [Factory](api:SilverStripe\Core\Injector\Factory) diff --git a/en/02_Developer_Guides/05_Extending/07_Templates.md b/en/02_Developer_Guides/05_Extending/07_Templates.md index 7e8ca71ce..525cbfef1 100644 --- a/en/02_Developer_Guides/05_Extending/07_Templates.md +++ b/en/02_Developer_Guides/05_Extending/07_Templates.md @@ -4,10 +4,10 @@ summary: Override templates from core and modules in your application icon: code --- -# Custom Templates +# Custom templates See [Template Inheritance](../templates). -## Form Templates +## Form templates -See [Form Templates](../forms/form_templates). \ No newline at end of file +See [Form Templates](../forms/form_templates). diff --git a/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md b/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md index d2f9c778f..57f089b20 100644 --- a/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md +++ b/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md @@ -4,24 +4,23 @@ summary: Have you created some work you think others can use? Turn it into a mod icon: rocket --- -# How to Publish a Silverstripe CMS module. +# How to publish a Silverstripe CMS module After you've [created](../modules#create) your own Silverstripe module, you could decide to make it open source and share it with the world. -If you wish to submit your module to our public directory, you take responsibility for a certain level of code quality, -adherence to conventions, writing documentation, and releasing updates. +If you wish to submit your module to our public directory, you take responsibility for a certain level of code quality, +adherence to conventions, writing documentation, and releasing updates. -Silverstripe CMS uses [Composer](../../../getting_started/composer/) to manage module releases and dependencies between -modules. If you plan on releasing your module to the public, ensure that you provide a `composer.json` file in the root +Silverstripe CMS uses [Composer](../../../getting_started/composer/) to manage module releases and dependencies between +modules. If you plan on releasing your module to the public, ensure that you provide a `composer.json` file in the root of your module containing the meta-data about your module. -For more information about what your `composer.json` file should include, consult the +For more information about what your `composer.json` file should include, consult the [Composer Documentation](https://getcomposer.org/doc/01-basic-usage.md). -**mycustommodule/composer.json** - ```json +// mycustommodule/composer.json { "name": "my-vendor/my-module", "description": "One-liner describing your module", @@ -30,7 +29,9 @@ For more information about what your `composer.json` file should include, consul "keywords": ["silverstripe", "some-tag", "some-other-tag"], "license": "BSD-3-Clause", "authors": [ - {"name": "Your Name","email": "your@email.com"} + { + "name": "Your Name","email": "your@email.com" + } ], "support": { "issues": "https://github.com/my-vendor/my-module/issues" @@ -41,13 +42,13 @@ For more information about what your `composer.json` file should include, consul }, "autoload": { "psr-4": { - "MyVendor\\MyModule\\": "src/" + "MyVendor\\MyModule\\": "src/" } }, "extra": { "installer-name": "my-module", "expose": [ - "client" + "client" ], "screenshots": [ "relative/path/screenshot1.png", @@ -57,15 +58,13 @@ For more information about what your `composer.json` file should include, consul } ``` - - -Once your module is published online with a service like github.com or bitbucket.com, submit the repository to +Once your module is published online with a service like GitHub.com or bitbucket.com, submit the repository to [Packagist](https://packagist.org/) to have the module accessible to developers. Note that Silverstripe CMS modules have the following distinct characteristics: - - Silverstripe CMS modules can be differentiated programatically from other packages by declaring `type: silverstripe-vendormodule`. - - Any folder which should be exposed to the public webroot must be declared in the `extra.expose` config. +- Silverstripe CMS modules can be differentiated programatically from other packages by declaring `type: silverstripe-vendormodule`. +- Any folder which should be exposed to the public webroot must be declared in the `extra.expose` config. These paths will be automatically rewritten to public urls which don't directly serve files from the `vendor` folder. For instance, `vendor/my-vendor/my-module/client` will be rewritten to `_resources/my-vendor/my-module/client`. See [Exposing static resources](/developer_guides/templates/requirements/#exposing-static-resources) @@ -73,24 +72,24 @@ Note that Silverstripe CMS modules have the following distinct characteristics: ## Releasing versions -Over time you may have to release new versions of your module to continue to work with newer versions of Silverstripe CMS. +Over time you may have to release new versions of your module to continue to work with newer versions of Silverstripe CMS. By using Composer, this is made easy for developers by allowing them to specify what version they want to use. Each -version of your module should be a separate branch in your version control and each branch should have a `composer.json` +version of your module should be a separate branch in your version control and each branch should have a `composer.json` file explicitly defining what versions of Silverstripe CMS you support. Say you have a module which supports Silverstripe CMS 5.0. A new release of this module takes advantage of new features -in Silverstripe CMS 5.1. In this case, you would create a new branch for the 5.0 compatible code base of your module. This +in Silverstripe CMS 5.1. In this case, you would create a new branch for the 5.0 compatible code base of your module. This allows you to continue fixing bugs on this older release branch. Other branches should be created on your module as needed if they're required to support specific Silverstripe CMS releases. -You can have an overlap in supported versions, e.g two branches in your module both support Silverstripe CMS 5.0. In this +You can have an overlap in supported versions, e.g two branches in your module both support Silverstripe CMS 5.0. In this case, you should explain the differences in your `README.md` file. Here's some common values for your `require` section (see [getcomposer.org](https://getcomposer.org/doc/01-basic-usage.md#package-versions) for details): - * `5.0.*`: Version `5.0`, including `5.0.1`, `5.0.2` etc, excluding `5.1` - * `~5.0`: Version `5.0` or higher, including `5.0.1` and `5.1` etc, excluding `6.0` - * `~5.0,<5.2`: Version `5.0` or higher, up until `5.2`, which is excluded - * `~5.0,>5.0.4`: Version `5.0` or higher, starting with `5.0.4` +- `5.0.*`: Version `5.0`, including `5.0.1`, `5.0.2` etc, excluding `5.1` +- `~5.0`: Version `5.0` or higher, including `5.0.1` and `5.1` etc, excluding `6.0` +- `~5.0,<5.2`: Version `5.0` or higher, up until `5.2`, which is excluded +- `~5.0,>5.0.4`: Version `5.0` or higher, starting with `5.0.4` diff --git a/en/02_Developer_Guides/05_Extending/How_Tos/02_Create_a_Google_Maps_Shortcode.md b/en/02_Developer_Guides/05_Extending/How_Tos/02_Create_a_Google_Maps_Shortcode.md index cc140105b..209ff5eb5 100644 --- a/en/02_Developer_Guides/05_Extending/How_Tos/02_Create_a_Google_Maps_Shortcode.md +++ b/en/02_Developer_Guides/05_Extending/How_Tos/02_Create_a_Google_Maps_Shortcode.md @@ -4,25 +4,23 @@ summary: Learn how to embed a Google map in the WYSIWYG editor with a simple sho icon: map --- -# How to Create a Google Maps Shortcode +# How to create a google maps shortcode -To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided +To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided address. We want our CMS authors to be able to embed the map using the following code: - ```html [googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap] ``` -So we've got the address as "content" of our new `googlemap` shortcode tags, plus some `width` and `height` arguments. +So we've got the address as "content" of our new `googlemap` shortcode tags, plus some `width` and `height` arguments. We'll add defaults to those in our shortcode parser so they're optional. -**app/_config.php** - ```php +// app/_config.php use SilverStripe\View\Parsers\ShortcodeParser; - -ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) { + +ShortcodeParser::get('default')->register('googlemap', function ($arguments, $address, $parser, $shortcode) { $iframeUrl = sprintf( '//maps.google.com/maps?q=%s&hnear=%s&ie=UTF8&hq=&t=m&z=14&output=embed', urlencode($address), @@ -33,7 +31,8 @@ ShortcodeParser::get('default')->register('googlemap', function($arguments, $add $height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300; return sprintf( - '', + '', $width, $height, $iframeUrl diff --git a/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md b/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md index a2c20ad99..abe2826d5 100644 --- a/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md +++ b/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md @@ -3,7 +3,7 @@ title: Track member logins summary: Keep a log in the database of who logs in and when icon: user-friends --- -# Howto: Track Member Logins +# Howto: track member logins Sometimes its good to know how active your users are, and when they last visited the site (and logged on). @@ -14,19 +14,18 @@ often the member has visited. Or more specifically, how often they have started a browser session, either through explicitly logging in or by invoking the "remember me" functionality. - ```php namespace App\Extension; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\ReadonlyField; -use SilverStripe\Security\Member; -use SilverStripe\Security\Security; +use SilverStripe\ORM\DB; use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\DB; +use SilverStripe\Security\Member; +use SilverStripe\Security\Security; -class MyMemberExtension extends DataExtension +class MyMemberExtension extends DataExtension { private static $db = [ 'LastVisited' => 'Datetime', @@ -36,7 +35,7 @@ class MyMemberExtension extends DataExtension /** * This extension hook is called every time a member is logged in */ - public function afterMemberLoggedIn() + public function afterMemberLoggedIn() { $this->logVisit(); } @@ -44,25 +43,27 @@ class MyMemberExtension extends DataExtension /** * This extension hook is called when a member's session is restored from "remember me" cookies */ - public function memberAutoLoggedIn() + public function memberAutoLoggedIn() { $this->logVisit(); } - public function updateCMSFields(FieldList $fields) + public function updateCMSFields(FieldList $fields) { $fields->addFieldsToTab('Root.Main', [ ReadonlyField::create('LastVisited', 'Last visited'), - ReadonlyField::create('NumVisit', 'Number of visits') + ReadonlyField::create('NumVisit', 'Number of visits'), ]); } - protected function logVisit() + protected function logVisit() { - if (!Security::database_is_ready()) return; + if (!Security::database_is_ready()) { + return; + } $lastVisitedTable = DataObject::getSchema()->tableForField(Member::class, 'LastVisited'); - + DB::query(sprintf( 'UPDATE "' . $lastVisitedTable . '" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d', DB::get_conn()->now(), diff --git a/en/02_Developer_Guides/05_Extending/How_Tos/index.md b/en/02_Developer_Guides/05_Extending/How_Tos/index.md index db2dfef2d..38d06fb58 100644 --- a/en/02_Developer_Guides/05_Extending/How_Tos/index.md +++ b/en/02_Developer_Guides/05_Extending/How_Tos/index.md @@ -1,6 +1,6 @@ --- title: How To's --- -# How To's: Extending Silverstripe CMS +# How to's: extending Silverstripe CMS [CHILDREN] diff --git a/en/02_Developer_Guides/05_Extending/index.md b/en/02_Developer_Guides/05_Extending/index.md index 20ddfb66e..35c4c6080 100644 --- a/en/02_Developer_Guides/05_Extending/index.md +++ b/en/02_Developer_Guides/05_Extending/index.md @@ -5,12 +5,14 @@ introduction: Silverstripe CMS is easily extensible to meet custom application r icon: code --- -No two applications are ever going to be the same and Silverstripe CMS is built with this in mind. The core framework -includes common functionality and default behaviors easily complemented with add-ons such as modules, widgets and -themes. +# Extending Silverstripe CMS -Silverstripe CMS includes a myriad of extension API's such as *Extension Hooks* and support for programming patterns -such as *Dependency Injection*. Allowing developers to tailor the framework to their needs without modifying the core +No two applications are ever going to be the same and Silverstripe CMS is built with this in mind. The core framework +includes common functionality and default behaviors easily complemented with add-ons such as modules, widgets and +themes. + +Silverstripe CMS includes a myriad of extension API's such as *Extension Hooks* and support for programming patterns +such as *Dependency Injection*. Allowing developers to tailor the framework to their needs without modifying the core framework. [CHILDREN Exclude="How_Tos"] diff --git a/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md b/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md index 9c7e84983..ba9920c48 100644 --- a/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md +++ b/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md @@ -3,90 +3,89 @@ title: Unit and Integration Testing summary: Test models, database logic and your object methods. --- -# Unit and Integration Testing +# Unit and integration testing -A Unit Test is an automated piece of code that invokes a unit of work in the application and then checks the behavior +A Unit Test is an automated piece of code that invokes a unit of work in the application and then checks the behavior to ensure that it works as it should. A simple example would be to test the result of a PHP method. -**app/src/Page.php** - - ```php -use SilverStripe\CMS\Model\SiteTree; +// app/src/Page.php +namespace { + use SilverStripe\CMS\Model\SiteTree; -class Page extends SiteTree -{ - public static function MyMethod() + class Page extends SiteTree { - return (1 + 1); + public static function myMethod() + { + return (1 + 1); + } } } ``` -**app/tests/PageTest.php** - - ```php +// app/tests/PageTest.php +namespace App\Test; + +use Page; use SilverStripe\Dev\SapphireTest; class PageTest extends SapphireTest { public function testMyMethod() { - $this->assertEquals(2, Page::MyMethod()); + $this->assertEquals(2, Page::myMethod()); } } ``` [info] -Tests for your application should be stored in the `app/tests` directory. Test cases for add-ons should be stored in -the `(modulename)/tests` directory. +Tests for your application should be stored in the `app/tests` directory. Test cases for add-ons should be stored in +the `(modulename)/tests` directory. -Test case classes should end with `Test` (e.g `PageTest`) and test methods must start with `test` (e.g `testMyMethod`). +Test case classes should end with `Test` (e.g. `PageTest`) and test methods must start with `test` (e.g. `testMyMethod`). Ensure you [import](https://php.net/manual/en/language.namespaces.importing.php#example-252) any classes you need for the test, including `SilverStripe\Dev\SapphireTest` or `SilverStripe\Dev\FunctionalTest`. [/info] -A Silverstripe CMS unit test is created by extending one of two classes, [SapphireTest](api:SilverStripe\Dev\SapphireTest) or [FunctionalTest](api:SilverStripe\Dev\FunctionalTest). +A Silverstripe CMS unit test is created by extending one of two classes, [SapphireTest](api:SilverStripe\Dev\SapphireTest) or [FunctionalTest](api:SilverStripe\Dev\FunctionalTest). -[SapphireTest](api:SilverStripe\Dev\SapphireTest) is used to test your model logic (such as a `DataObject`), and [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) is used when +[SapphireTest](api:SilverStripe\Dev\SapphireTest) is used to test your model logic (such as a `DataObject`), and [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) is used when you want to test a `Controller`, `Form` or anything that requires a web page. [info] `FunctionalTest` is a subclass of `SapphireTest` so will inherit all of the behaviors. By subclassing `FunctionalTest` -you gain the ability to load and test web pages on the site. +you gain the ability to load and test web pages on the site. -`SapphireTest` in turn, extends `PHPUnit\Framework\TestCase`. For more information on `PHPUnit\Framework\TestCase` see -the [PHPUnit](https://www.phpunit.de) documentation. It provides a lot of fundamental concepts that we build on in this +`SapphireTest` in turn, extends `PHPUnit\Framework\TestCase`. For more information on `PHPUnit\Framework\TestCase` see +the [PHPUnit](https://www.phpunit.de) documentation. It provides a lot of fundamental concepts that we build on in this documentation. [/info] -## Test Databases and Fixtures +## Test databases and fixtures -Silverstripe CMS tests create their own database when the test starts and fixture files are specified. New `ss_tmp` databases are created using the same -connection details you provide for the main website. The new `ss_tmp` database does not copy what is currently in your +Silverstripe CMS tests create their own database when the test starts and fixture files are specified. New `ss_tmp` databases are created using the same +connection details you provide for the main website. The new `ss_tmp` database does not copy what is currently in your application database. To provide seed data use a [Fixture](fixtures) file. [alert] -As the test runner will create new databases for the tests to run, the database user should have the appropriate +As the test runner will create new databases for the tests to run, the database user should have the appropriate permissions to create new databases on your server. [/alert] [notice] -The test database is rebuilt every time one of the test methods is run and is removed afterwards. If the test is interrupted, the database will not be removed. Over time, you may have several hundred test +The test database is rebuilt every time one of the test methods is run and is removed afterwards. If the test is interrupted, the database will not be removed. Over time, you may have several hundred test databases on your machine. To get rid of them, run `sake dev/tasks/CleanupTestDatabasesTask`. [/notice] -## Custom PHPUnit Configuration +## Custom PHPUnit configuration -The `phpunit` executable can be configured by command line arguments or through an XML file. Silverstripe CMS comes with a -default `phpunit.xml.dist` that you can use as a starting point. Copy the file into `phpunit.xml` and customize to your +The `phpunit` executable can be configured by command line arguments or through an XML file. Silverstripe CMS comes with a +default `phpunit.xml.dist` that you can use as a starting point. Copy the file into `phpunit.xml` and customize to your needs. -**phpunit.xml** - - ```xml + @@ -101,13 +100,15 @@ needs. ``` -### setUp() and tearDown() +### SetUp() and tearDown() In addition to loading data through a [Fixture File](fixtures), a test case may require some additional setup work to be -run before each test method. For this, use the PHPUnit `setUp` and `tearDown` methods. These are run at the start and +run before each test method. For this, use the PHPUnit `setUp` and `tearDown` methods. These are run at the start and end of each test. ```php +namespace App\Test; + use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\Versioned\Versioned; @@ -133,22 +134,23 @@ class PageTest extends SapphireTest public function testMyMethod() { - // .. + // ... } public function testMySecondMethod() { - // .. + // ... } } ``` -`tearDownAfterClass` and `setUpBeforeClass` can be used to run code just once for the file rather than before and after +`tearDownAfterClass` and `setUpBeforeClass` can be used to run code just once for the file rather than before and after each individual test case. Remember to class the parent method in each method to ensure the core boot-strapping of tests takes place. - ```php +namespace App\Test; + use SilverStripe\Dev\SapphireTest; class PageTest extends SapphireTest @@ -157,19 +159,21 @@ class PageTest extends SapphireTest { parent::setUpBeforeClass(); - // .. + // ... } public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); - // .. + // ... } + + // ... } ``` -### Config and Injector Nesting +### Config and injector nesting A powerful feature of both [`Config`](/developer_guides/configuration/configuration/) and [`Injector`](/developer_guides/extending/injector/) is the ability to "nest" them so that you can make changes that can easily be discarded without having to manage previous values. @@ -179,34 +183,42 @@ If you need to make changes to `Config` (or `Injector`) for each test (or the wh It's important to remember that the `parent::setUp();` functions will need to be called first to ensure the nesting feature works as expected. - ```php -public static function setUpBeforeClass(): void -{ - parent::setUpBeforeClass(); - //this will remain for the whole suite and be removed for any other tests - Config::inst()->update('ClassName', 'var_name', 'var_value'); -} +namespace App\Test; -public function testFeatureDoesAsExpected() -{ - //this will be reset to 'var_value' at the end of this test function - Config::inst()->update('ClassName', 'var_name', 'new_var_value'); -} +use SilverStripe\Core\Config\Config; +use SilverStripe\Dev\SapphireTest; -public function testAnotherFeatureDoesAsExpected() +class MyTest extends SapphireTest { - Config::inst()->get('ClassName', 'var_name'); // this will be 'var_value' + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + //this will remain for the whole suite and be removed for any other tests + Config::inst()->update('ClassName', 'var_name', 'var_value'); + } + + public function testFeatureDoesAsExpected() + { + //this will be reset to 'var_value' at the end of this test function + Config::inst()->update('ClassName', 'var_name', 'new_var_value'); + } + + public function testAnotherFeatureDoesAsExpected() + { + // this will be 'var_value' + Config::inst()->get('ClassName', 'var_name'); + } } ``` -## Related Documentation +## Related documentation -* [How to Write a SapphireTest](how_tos/write_a_sapphiretest) -* [How to Write a FunctionalTest](how_tos/write_a_functionaltest) -* [Fixtures](fixtures) +- [How to Write a SapphireTest](how_tos/write_a_sapphiretest) +- [How to Write a FunctionalTest](how_tos/write_a_functionaltest) +- [Fixtures](fixtures) -## API Documentation +## API documentation -* [SapphireTest](api:SilverStripe\Dev\SapphireTest) -* [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) +- [SapphireTest](api:SilverStripe\Dev\SapphireTest) +- [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) diff --git a/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md b/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md index a842e86a6..71e65c000 100644 --- a/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md +++ b/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md @@ -3,10 +3,10 @@ title: Functional Testing summary: Test controllers, forms and HTTP responses. --- -# Functional Testing +# Functional testing -[FunctionalTest](api:SilverStripe\Dev\FunctionalTest) test your applications `Controller` logic and anything else which requires a web request. The -core idea of these tests is the same as `SapphireTest` unit tests but `FunctionalTest` adds several methods for +[FunctionalTest](api:SilverStripe\Dev\FunctionalTest) test your applications `Controller` logic and anything else which requires a web request. The +core idea of these tests is the same as `SapphireTest` unit tests but `FunctionalTest` adds several methods for creating [HTTPRequest](api:SilverStripe\Control\HTTPRequest), receiving [HTTPResponse](api:SilverStripe\Control\HTTPResponse) objects and modifying the current user session. ## Get @@ -15,35 +15,35 @@ creating [HTTPRequest](api:SilverStripe\Control\HTTPRequest), receiving [HTTPRes $page = $this->get($url); ``` -Performs a GET request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value +Performs a GET request on `$url` and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value of the response. ## Post + ```php $page = $this->post($url); ``` -Performs a POST request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value +Performs a POST request on `$url` and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value of the response. -## Other Requests +## Other requests + ```php $page = $this->sendRequest('PUT', $url); ``` -Performs a request on $url with the HTTP method provided (useful for PUT, PATCH, DELETE, etc.). This also changes the current page to the value of the response. +Performs a request on `$url` with the HTTP method provided (useful for PUT, PATCH, DELETE, etc.). This also changes the current page to the value of the response. ## Submit - ```php $submit = $this->submitForm($formID, $button = null, $data = []); ``` Submits the given form (`#ContactForm`) on the current page and returns the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). -## LogInAs - +## `LogInAs` ```php $this->logInAs($member); @@ -53,7 +53,7 @@ Logs a given user in, sets the current session. When doing a functional testing it's important to use `$this->logInAs($member);` rather than simply `Security::setCurrentUser($member);` or `$this->session()->set('loggedInAs', $member->ID);` as the latter two will not run any logic contained inside login authenticators. -## LogOut +## `LogOut` Log out the current user, destroys the current session. @@ -65,68 +65,66 @@ $this->logOut(); The `FunctionalTest` class also provides additional asserts to validate your tests. -### assertPartialMatchBySelector - +### `assertPartialMatchBySelector` ```php -$this->assertPartialMatchBySelector('p.good',[ - 'Test save was successful' +$this->assertPartialMatchBySelector('p.good', [ + 'Test save was successful', ]); ``` -Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS -selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The +Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS +selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The assertion fails if one of the expectedMatches fails to appear. - -### assertExactMatchBySelector - +### `assertExactMatchBySelector` ```php -$this->assertExactMatchBySelector("#MyForm_ID p.error", [ - "That email address is invalid." +$this->assertExactMatchBySelector('#MyForm_ID p.error', [ + 'That email address is invalid.', ]); ``` -Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS -selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The -assertion fails if one of the expectedMatches fails to appear. +Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS +selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The +assertion fails if one of the expectedMatches fails to appear. -### assertPartialHTMLMatchBySelector +### `assertPartialHTMLMatchBySelector` ```php -$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", [ - "That email address is invalid." +$this->assertPartialHTMLMatchBySelector('#MyForm_ID p.error', [ + 'That email address is invalid.', ]); ``` -Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS -selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The +Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS +selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The assertion fails if one of the expectedMatches fails to appear. [notice] ` ` characters are stripped from the content; make sure that your assertions take this into account. [/notice] -### assertExactHTMLMatchBySelector +### `assertExactHTMLMatchBySelector` + ```php -$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", [ - "That email address is invalid." +$this->assertExactHTMLMatchBySelector('#MyForm_ID p.error', [ + 'That email address is invalid.', ]); ``` -Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS -selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The +Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS +selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The assertion fails if one of the expectedMatches fails to appear. [notice] ` ` characters are stripped from the content; make sure that your assertions take this into account. [/notice] -## Related Documentation +## Related documentation -* [How to write a FunctionalTest](how_tos/write_a_functionaltest) +- [How to write a FunctionalTest](how_tos/write_a_functionaltest) -## API Documentation +## API documentation -* [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) +- [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) diff --git a/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md b/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md index f483328dd..03629edf4 100644 --- a/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md +++ b/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md @@ -3,7 +3,7 @@ title: Behavior Testing summary: Describe how your application should behave in plain text and run tests in a browser. --- -# Behavior Testing +# Behavior testing -For behavior testing in Silverstripe CMS, check out +For behavior testing in Silverstripe CMS, check out [Silverstripe CMS Behat Documentation](https://github.com/silverstripe/silverstripe-behat-extension/). diff --git a/en/02_Developer_Guides/06_Testing/04_Fixtures.md b/en/02_Developer_Guides/06_Testing/04_Fixtures.md index 668b9d69a..9a4ceee64 100644 --- a/en/02_Developer_Guides/06_Testing/04_Fixtures.md +++ b/en/02_Developer_Guides/06_Testing/04_Fixtures.md @@ -13,40 +13,44 @@ fixtures - all we have to do is define them. To include your fixture file in your tests, you should define it as your `$fixture_file`: - -**app/tests/MyNewTest.php** - - ```php +// app/tests/MyNewTest.php +namespace App\Test; + use SilverStripe\Dev\SapphireTest; class MyNewTest extends SapphireTest { protected static $fixture_file = 'fixtures.yml'; + + // ... } ``` You can also use an array of fixture files, if you want to use parts of multiple other tests. -If you are using [api:SilverStripe\Dev\TestOnly] dataobjects in your fixtures, you must +If you are using [`TestOnly`](api:SilverStripe\Dev\TestOnly) dataobjects in your fixtures, you must declare these classes within the $extra_dataobjects variable. -**app/tests/MyNewTest.php** - ```php +// app/tests/MyNewTest.php +namespace App\Test; + use SilverStripe\Dev\SapphireTest; class MyNewTest extends SapphireTest { protected static $fixture_file = [ 'fixtures.yml', - 'otherfixtures.yml' + 'otherfixtures.yml', ]; protected static $extra_dataobjects = [ Player::class, Team::class, ]; + + // ... } ``` @@ -55,58 +59,64 @@ Typically, you'd have a separate fixture file for each class you are testing - a Fixtures are defined in `YAML`. `YAML` is a markup language which is deliberately simple and easy to read, so it is ideal for fixture generation. Say we have the following two DataObjects: - ```php -use SilverStripe\ORM\DataObject; +namespace App\Test; + use SilverStripe\Dev\TestOnly; +use SilverStripe\ORM\DataObject; class Player extends DataObject implements TestOnly { private static $db = [ - 'Name' => 'Varchar(255)' + 'Name' => 'Varchar(255)', ]; private static $has_one = [ - 'Team' => 'Team' + 'Team' => Team::class, ]; } +``` + +```php +namespace App\Test; + +use SilverStripe\Dev\TestOnly; +use SilverStripe\ORM\DataObject; class Team extends DataObject implements TestOnly { private static $db = [ 'Name' => 'Varchar(255)', - 'Origin' => 'Varchar(255)' + 'Origin' => 'Varchar(255)', ]; private static $has_many = [ - 'Players' => 'Player' + 'Players' => Player::class, ]; } ``` We can represent multiple instances of them in `YAML` as follows: -**app/tests/fixtures.yml** - ```yml - -Team: +# app/tests/fixtures.yml +App\Test\Team: hurricanes: Name: The Hurricanes Origin: Wellington crusaders: Name: The Crusaders Origin: Canterbury -Player: +App\Test\Player: john: Name: John - Team: =>Team.hurricanes + Team: =>App\Test\Team.hurricanes joe: Name: Joe - Team: =>Team.crusaders + Team: =>App\Test\Team.crusaders jack: Name: Jack - Team: =>Team.crusaders + Team: =>App\Test\Team.crusaders ``` This `YAML` is broken up into three levels, signified by the indentation of each line. In the first level of @@ -115,9 +125,10 @@ indentation, `Player` and `Team`, represent the class names of the objects we wa The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify represents a new object and can be referenced in the PHP using `objFromFixture` - ```php -$player = $this->objFromFixture('Player', 'jack'); +use App\Test\Player; + +$player = $this->objFromFixture(Player::class, 'jack'); ``` The third and final level represents each individual object's fields. @@ -126,10 +137,10 @@ A field can either be provided with raw data (such as the names for our Players) seen by the fields prefixed with `=>`. Each one of our Players has a relationship to a Team, this is shown with the `Team` field for each `Player` being set -to `=>Team.` followed by a team name. +to `=>App\Test\Team.` followed by a team name. [info] -Take the player John in our example YAML, his team is the Hurricanes which is represented by `=>Team.hurricanes`. This +Take the player John in our example YAML, his team is the Hurricanes which is represented by `=>App\Test\Team.hurricanes`. This sets the `has_one` relationship for John with with the `Team` object `hurricanes`. [/info] @@ -139,42 +150,42 @@ database field (TeamID). [/hint] [hint] -Also be aware the target of a relationship must be defined before it is referenced, for example the `hurricanes` team must appear in the fixture file before the line `Team: =>Team.hurricanes`. +Also be aware the target of a relationship must be defined before it is referenced, for example the `hurricanes` team must appear in the fixture file before the line `Team: =>App\Test\Team.hurricanes`. [/hint] -This style of relationship declaration can be used for any type of relationship (i.e.`has_one`, `has_many`, `many_many`). +This style of relationship declaration can be used for any type of relationship (i.e. `has_one`, `has_many`, `many_many`). We can also declare the relationships conversely. Another way we could write the previous example is: - ```yml -Player: +App\Test\Player: john: Name: John joe: Name: Joe jack: Name: Jack -Team: +App\Test\Team: hurricanes: Name: Hurricanes Origin: Wellington - Players: =>Player.john + Players: =>App\Test\Player.john crusaders: Name: Crusaders Origin: Canterbury - Players: =>Player.joe,=>Player.jack + Players: =>App\Test\Player.joe,=>App\Test\Player.jack ``` The database is populated by instantiating `DataObject` objects and setting the fields declared in the `YAML`, then calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to writing: - ```php -$team = new Team([ +use App\Test\Team; + +$team = Team::create([ 'Name' => 'Hurricanes', - 'Origin' => 'Wellington' + 'Origin' => 'Wellington', ]); $team->write(); @@ -187,92 +198,97 @@ As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value l test. [/notice] -### Fixtures for namespaced classes +## Fixtures for namespaced classes You will need to use fully qualified class names in your YAML fixture files. In the above examples, they belong to the global namespace so there is nothing requires, but if you have a deeper DataObject, or it has a relationship to models that are part of the framework for example, you will need to include their namespaces: - ```yml -MyProject\Model\Player: +App\Test\Player: john: Name: join -MyProject\Model\Team: +App\Test\Team: crusaders: Name: Crusaders Origin: Canterbury - Players: =>MyProject\Model\Player.john + Players: =>App\Test\Player.john ``` [notice] If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. See [DataObject](api:SilverStripe\ORM\DataObject) for an example. [/notice] -### Defining many_many_extraFields +## Defining many_many_extraFields `many_many` relations can have additional database fields attached to the relationship. For example we may want to declare the role each player has in the team. - ```php +namespace App\Test; + use SilverStripe\ORM\DataObject; class Player extends DataObject { private static $db = [ - 'Name' => 'Varchar(255)' + 'Name' => 'Varchar(255)', ]; private static $belongs_many_many = [ - 'Teams' => 'Team' + 'Teams' => Team::class, ]; } +``` + +```php +namespace App\Test; + +use SilverStripe\ORM\DataObject; class Team extends DataObject { private static $db = [ - 'Name' => 'Varchar(255)' + 'Name' => 'Varchar(255)', ]; private static $many_many = [ - 'Players' => 'Player' + 'Players' => Player::class, ]; private static $many_many_extraFields = [ 'Players' => [ - 'Role' => "Varchar" - ] + 'Role' => 'Varchar', + ], ]; } ``` To provide the value for the `many_many_extraField` use the YAML list syntax. - ```yml -Player: +App\Test\Player: john: Name: John joe: Name: Joe jack: Name: Jack -Team: +App\Test\Team: hurricanes: Name: The Hurricanes Players: - - =>Player.john: + - =>App\Test\Player.john: Role: Captain crusaders: Name: The Crusaders Players: - - =>Player.joe: + - =>App\Test\Player.joe: Role: Captain - - =>Player.jack: + - =>App\Test\Player.jack: Role: Winger ``` -## Fixture Factories +## Fixture factories While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention. @@ -290,21 +306,22 @@ name, which is usually set to the class it creates such as `Member` or `Page`. Blueprints are auto-created for all available DataObject subclasses, you only need to instantiate a factory to start using them. - ```php +use App\Test\Team; use SilverStripe\Core\Injector\Injector; $factory = Injector::inst()->create('FixtureFactory'); -$obj = $factory->createObject('Team', 'hurricanes'); +$obj = $factory->createObject(Team::class, 'hurricanes'); ``` In order to create an object with certain properties, just add a third argument: - ```php -$obj = $factory->createObject('Team', 'hurricanes', [ - 'Name' => 'My Value' +use App\Test\Team; + +$obj = $factory->createObject(Team::class, 'hurricanes', [ + 'Name' => 'My Value', ]); ``` @@ -315,40 +332,46 @@ mapped to their database identifiers. After we've created this object in the factory, `getId` is used to retrieve it by the identifier. - ```php -$databaseId = $factory->getId('Team', 'hurricanes'); +use App\Test\Team; + +$databaseId = $factory->getId(Team::class, 'hurricanes'); ``` -### Default Properties +### Default properties Blueprints can be overwritten in order to customise their behavior. For example, if a Fixture does not provide a Team name, we can set the default to be `Unknown Team`. - ```php -$factory->define('Team', [ - 'Name' => 'Unknown Team' +use App\Test\Team; + +$factory->define(Team::class, [ + 'Name' => 'Unknown Team', ]); ``` -### Dependent Properties +### Dependent properties Values can be set on demand through anonymous functions, which can either generate random defaults, or create composite values based on other fixture data. - ```php -$factory->define('Member', [ - 'Email' => function($obj, $data, $fixtures) { - if(isset($data['FirstName']) { - $obj->Email = strtolower($data['FirstName']) . '@example.com'; - } - }, - 'Score' => function($obj, $data, $fixtures) { - $obj->Score = rand(0,10); - } -)]; +use SilverStripe\Security\Member; + +$factory->define( + Member::class, + [ + 'Email' => function ($obj, $data, $fixtures) { + if (isset($data['FirstName'])) { + $obj->Email = strtolower($data['FirstName']) . '@example.com'; + } + }, + 'Score' => function ($obj, $data, $fixtures) { + $obj->Score = rand(0, 10); + }, + ] +); ``` ### Relations @@ -356,10 +379,11 @@ $factory->define('Member', [ Model relations can be expressed through the same notation as in the YAML fixture format described earlier, through the `=>` prefix on data values. - ```php -$obj = $factory->createObject('Team', 'hurricanes', [ - 'MyHasManyRelation' => '=>Player.john,=>Player.joe' +use App\Test\Team; + +$obj = $factory->createObject(Team::class, 'hurricanes', [ + 'MyHasManyRelation' => '=>App\Test\Player.john,=>App\Test\Player.joe', ]); ``` @@ -368,51 +392,56 @@ $obj = $factory->createObject('Team', 'hurricanes', [ Sometimes new model instances need to be modified in ways which can't be expressed in their properties, for example to publish a page, which requires a method call. - ```php $blueprint = Injector::inst()->create('FixtureBlueprint', 'Member'); -$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { +$blueprint->addCallback('afterCreate', function ($obj, $identifier, $data, $fixtures) { $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); }); -$page = $factory->define('Page', $blueprint); +$page = $factory->define(Page::class, $blueprint); ``` Available callbacks: - * `beforeCreate($identifier, $data, $fixtures)` - * `afterCreate($obj, $identifier, $data, $fixtures)` +- `beforeCreate($identifier, $data, $fixtures)` +- `afterCreate($obj, $identifier, $data, $fixtures)` -### Multiple Blueprints +### Multiple blueprints Data of the same type can have variations, for example forum members vs. CMS admins could both inherit from the `Member` class, but have completely different properties. This is where named blueprints come in. By default, blueprint names equal the class names they manage. - ```php -$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member'); +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Dev\FixtureBlueprint; +use SilverStripe\Security\Group; +use SilverStripe\Security\Member; + +$memberBlueprint = Injector::inst()->create(FixtureBlueprint::class, 'Member', Member::class); -$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member'); +$adminBlueprint = Injector::inst()->create(FixtureBlueprint::class, 'AdminMember', Member::class); -$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { - if(isset($fixtures['Group']['admin'])) { +$adminBlueprint->addCallback('afterCreate', function ($obj, $identifier, $data, $fixtures) { + if (isset($fixtures['Group']['admin'])) { $adminGroup = Group::get()->byId($fixtures['Group']['admin']); $obj->Groups()->add($adminGroup); } }); -$member = $factory->createObject('Member'); // not in admin group +// not in admin group +$member = $factory->createObject('Member'); -$admin = $factory->createObject('AdminMember'); // in admin group +// in admin group +$admin = $factory->createObject('AdminMember'); ``` -## Related Documentation +## Related documentation -* [How to use a FixtureFactory](how_tos/fixturefactories/) +- [How to use a FixtureFactory](how_tos/fixturefactories/) -## API Documentation +## API documentation -* [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) -* [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint) +- [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) +- [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint) diff --git a/en/02_Developer_Guides/06_Testing/05_Testing_Glossary.md b/en/02_Developer_Guides/06_Testing/05_Testing_Glossary.md index 39868497b..70de14842 100644 --- a/en/02_Developer_Guides/06_Testing/05_Testing_Glossary.md +++ b/en/02_Developer_Guides/06_Testing/05_Testing_Glossary.md @@ -3,12 +3,14 @@ title: Testing Glossary summary: All the jargon you need to be a bonafide testing guru --- +# Testing glossary +
    Assertion
    A predicate statement that must be true when a test runs.
    Behat
    -
    A behaviour-driven testing library used with Silverstripe CMS as a higher-level alternative to the FunctionalTest API, see https://behat.org.
    +
    A behaviour-driven testing library used with Silverstripe CMS as a higher-level alternative to the `FunctionalTest` API, see .
    Test Case
    The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the base test case.
    @@ -17,7 +19,7 @@ summary: All the jargon you need to be a bonafide testing guru
    Also known as a 'test group', a composite of test cases, used to collect individual unit tests into packages, allowing all tests to be run at once.
    Fixture
    -
    Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be used to create fixtures for the duration of a test - setUp - and clean them up after the test is done - tearDown.
    +
    Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown`.
    Refactoring
    A behavior preserving transformation of code. If you change the code, while keeping the actual functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
    @@ -41,5 +43,5 @@ summary: All the jargon you need to be a bonafide testing guru
    A style of programming where tests for a new feature are constructed before any code is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to understand the problem space and discover suitable APIs for performing specific actions.
    Behavior Driven Development (BDD)
    -
    An extension of the test-driven programming style, where tests are used primarily for describing the specification of how code should perform. In practice, there's little or no technical difference - it all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so Specification is used in place of Test Case, and should is used in place of expect and assert.
    +
    An extension of the test-driven programming style, where tests are used primarily for describing the specification of how code should perform. In practice, there's little or no technical difference - it all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so *Specification* is used in place of *Test Case*, and *should* is used in place of *expect* and *assert*.
    diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md b/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md index 7f28d9e16..89b89610e 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md @@ -3,19 +3,21 @@ title: How to write a SapphireTest summary: Learn the basics of unit testing in Silverstripe --- -# How to write a SapphireTest +# How to write a sapphireTest Here is an example of a test which extends [SapphireTest](api:SilverStripe\Dev\SapphireTest) to test the URL generation of the page. It also showcases how you can load default records into the test database. -**app/tests/PageTest.php** - ```php +// app/tests/PageTest.php +namespace App\Test; + +use Page; use SilverStripe\Dev\SapphireTest; class PageTest extends SapphireTest { - /** + /** * Defines the fixture file to use for this test class * @var string $fixture_file */ @@ -35,11 +37,11 @@ class PageTest extends SapphireTest 'home' => 'home', 'staff' => 'my-staff', 'about' => 'about-us', - 'staffduplicate' => 'my-staff-2' + 'staffduplicate' => 'my-staff-2', ]; foreach ($expectedURLs as $fixture => $urlSegment) { - $obj = $this->objFromFixture('Page', $fixture); + $obj = $this->objFromFixture(Page::class, $fixture); $this->assertEquals($urlSegment, $obj->URLSegment); } @@ -47,9 +49,8 @@ class PageTest extends SapphireTest } ``` -**app/tests/SiteTreeTest.yml** - -```yaml +```yml +# app/tests/SiteTreeTest.yml Page: home: Title: Home @@ -61,9 +62,8 @@ Page: Title: My Staff ``` -**phpunit.xml** - ```xml + @@ -82,16 +82,16 @@ Page: ``` Firstly we define a static `$fixture_file`, this should point to a file that represents the data we want to test, -represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test +represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test database and discarded at the end of the test. [notice] -The `fixture_file` property can be path to a file, or an array of strings pointing to many files. The path must be +The `fixture_file` property can be path to a file, or an array of strings pointing to many files. The path must be absolute from your project root folder. [/notice] -The second part of our class is the `testURLGeneration` method. This method is our test. When the test is executed, -methods prefixed with the word `test` will be run. +The second part of our class is the `testURLGeneration` method. This method is our test. When the test is executed, +methods prefixed with the word `test` will be run. [notice] The test database is rebuilt every time one of these methods is run. @@ -103,7 +103,7 @@ but not saved in the database anywhere, `objFromFixture` looks the [DataObject]( database. This means that you can use it to test the functions responsible for looking up content in the database. The final part of our test is an assertion command, `assertEquals`. An assertion command allows us to test for something -in our test methods (in this case we are testing if two values are equal). A test method can have more than one +in our test methods (in this case we are testing if two values are equal). A test method can have more than one assertion command, and if any one of these assertions fail, so will the test method. The example **phpunit.xml** file should be placed in the root folder of your project. PHPUnit 9 should be included by default, as a dev dependency, in the **composer.json** file. @@ -112,7 +112,7 @@ The example **phpunit.xml** file should be placed in the root folder of your pro Just like on web requests, Silverstripe CMS caches metadata about the execution context. This cache can get stale, e.g. when you change YAML configuration or add certain types of PHP code. In order to flush the cache, the **first time** this test is run use the `flush=1` CLI parameter: -```sh +```bash vendor/bin/phpunit app/tests/PageTest.php '' flush=1 ``` @@ -120,12 +120,12 @@ vendor/bin/phpunit app/tests/PageTest.php '' flush=1 For more information on PHPUnit's assertions see the [PHPUnit manual](https://docs.phpunit.de/en/9.6/assertions.html). [/info] -## Related Documentation +## Related documentation -* [Unit Testing](../unit_testing) -* [Fixtures](../fixtures) +- [Unit Testing](../unit_testing) +- [Fixtures](../fixtures) -## API Documentation +## API documentation -* [SapphireTest](api:SilverStripe\Dev\SapphireTest) -* [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) +- [SapphireTest](api:SilverStripe\Dev\SapphireTest) +- [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md b/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md index 8760c0a6d..0410e8066 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md @@ -3,26 +3,25 @@ title: How to write a FunctionalTest summary: Expand your testing capabilities with integrations tests --- -# How to Write a FunctionalTest +# How to write a functionalTest -[FunctionalTest](api:SilverStripe\Dev\FunctionalTest) test your applications `Controller` instances and anything else which requires a web request. The +[FunctionalTest](api:SilverStripe\Dev\FunctionalTest) test your applications `Controller` instances and anything else which requires a web request. The core of these tests are the same as `SapphireTest` unit tests but add several methods for creating [HTTPRequest](api:SilverStripe\Control\HTTPRequest) and receiving [HTTPResponse](api:SilverStripe\Control\HTTPResponse) objects. In this How To, we'll see how to write a test to query a page, check the response and modify the session within a test. -**app/tests/HomePageTest.php** - - ```php +// app/tests/HomePageTest.php +namespace App\Test; + use SilverStripe\Security\Member; -class HomePageTest extends FunctionalTest +class HomePageTest extends FunctionalTest { - /** * Test generation of the view */ - public function testViewHomePage() + public function testViewHomePage() { $page = $this->get('home/'); @@ -30,14 +29,14 @@ class HomePageTest extends FunctionalTest $this->assertEquals(200, $page->getStatusCode()); // We should see a login form - $login = $this->submitForm("LoginFormID", null, [ + $login = $this->submitForm('LoginFormID', null, [ 'Email' => 'test@example.com', - 'Password' => 'wrongpassword' + 'Password' => 'wrongpassword', ]); // wrong details, should now see an error message - $this->assertExactHTMLMatchBySelector("#LoginForm p.error", [ - "That email address is invalid." + $this->assertExactHTMLMatchBySelector('#LoginForm p.error', [ + 'That email address is invalid.', ]); // If we login as a user we should see a welcome message @@ -46,18 +45,18 @@ class HomePageTest extends FunctionalTest $this->logInAs($me); $page = $this->get('home/'); - $this->assertExactHTMLMatchBySelector("#Welcome", [ - 'Welcome back' + $this->assertExactHTMLMatchBySelector('#Welcome', [ + 'Welcome back', ]); } } ``` -## Related Documentation +## Related documentation -* [Functional Testing](../functional_testing) -* [Unit Testing](../unit_testing) +- [Functional Testing](../functional_testing) +- [Unit Testing](../unit_testing) -## API Documentation +## API documentation -* [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) +- [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md b/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md index 3ed7497a6..86a81310b 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md @@ -4,7 +4,7 @@ summary: Provide context to your tests with database fixtures icon: industry --- -# How to use a FixtureFactory +# How to use a fixtureFactory The [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) is used to manually create data structures for use with tests. For more information on fixtures see the [Fixtures](../fixtures) documentation. @@ -12,32 +12,35 @@ see the [Fixtures](../fixtures) documentation. In this how to we'll use a `FixtureFactory` and a custom blue print for giving us a shortcut for creating new objects with information that we need. - ```php -use SilverStripe\Dev\SapphireTest; +namespace App\Test; + +use App\Model\MyObject; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Dev\SapphireTest; -class MyObjectTest extends SapphireTest +class MyObjectTest extends SapphireTest { + protected FixtureFactory $factory; - protected $factory; - - function __construct() { + public function __construct() + { parent::__construct(); - $factory = Injector::inst()->create('FixtureFactory'); + $factory = Injector::inst()->create(FixtureFactory::class); // Defines a "blueprint" for new objects - $factory->define('MyObject', [ - 'MyProperty' => 'My Default Value' + $factory->define(MyObject::class, [ + 'MyProperty' => 'My Default Value', ]); $this->factory = $factory; } - function testSomething() { + public function testSomething() + { $MyObjectObj = $this->factory->createObject( - 'MyObject', + MyObject::class, ['MyOtherProperty' => 'My Custom Value'] ); @@ -50,11 +53,11 @@ class MyObjectTest extends SapphireTest } ``` -## Related Documentation +## Related documentation -* [Fixtures](../fixtures) +- [Fixtures](../fixtures) -## API Documentation +## API documentation -* [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) -* [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint) +- [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) +- [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint) diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md b/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md index d555af0bb..26d0440f0 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md @@ -4,47 +4,44 @@ summary: Test email functionality without ever hitting an inbox icon: envelope --- -# Testing Email within Unit Tests +# Testing email within unit tests -Silverstripe CMS's test system has built-in support for testing emails sent using the [Email](api:SilverStripe\Control\Email\Email) class. If you are -running a [SapphireTest](api:SilverStripe\Dev\SapphireTest) test, then it holds off actually sending the email, and instead lets you assert that an +Silverstripe CMS's test system has built-in support for testing emails sent using the [Email](api:SilverStripe\Control\Email\Email) class. If you are +running a [SapphireTest](api:SilverStripe\Dev\SapphireTest) test, then it holds off actually sending the email, and instead lets you assert that an email was sent using this method. - ```php use SilverStripe\Control\Email\Email; -public function MyMethod() +public function MyMethod() { $e = new Email(); - $e->To = "someone@example.com"; - $e->Subject = "Hi there"; - $e->Body = "I just really wanted to email you and say hi."; + $e->To = 'someone@example.com'; + $e->Subject = 'Hi there'; + $e->Body = 'I just really wanted to email you and say hi.'; $e->send(); } ``` To test that `MyMethod` sends the correct email, use the [SapphireTest::assertEmailSent()](api:SilverStripe\Dev\SapphireTest::assertEmailSent()) method. - ```php $this->assertEmailSent($to, $from, $subject, $body); // to assert that the email is sent to the correct person -$this->assertEmailSent("someone@example.com", null, "/th.*e$/"); +$this->assertEmailSent('someone@example.com', null, '/th.*e$/'); ``` Each of the arguments (`$to`, `$from`, `$subject` and `$body`) can be either one of the following. -* A string: match exactly that string -* `null/false`: match anything -* A PERL regular expression (starting with '/') - -## Related Documentation +- A string: match exactly that string +- `null/false`: match anything +- A PERL regular expression (starting with '/') -* [Email](../../email) +## Related documentation -## API Documentation +- [Email](../../email) -* [Email](api:SilverStripe\Control\Email\Email) +## API documentation +- [Email](api:SilverStripe\Control\Email\Email) diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/index.md b/en/02_Developer_Guides/06_Testing/How_Tos/index.md index 4dcf0bce3..6380e6bcf 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/index.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/index.md @@ -1,6 +1,6 @@ --- title: How To's --- -# How To's: Testing +# How to's: testing -[CHILDREN] \ No newline at end of file +[CHILDREN] diff --git a/en/02_Developer_Guides/06_Testing/index.md b/en/02_Developer_Guides/06_Testing/index.md index d5d43ac99..00a668a51 100644 --- a/en/02_Developer_Guides/06_Testing/index.md +++ b/en/02_Developer_Guides/06_Testing/index.md @@ -18,22 +18,22 @@ the [Testing Glossary](testing_glossary). To get started now, follow the install You should also read over the [PHPUnit manual](https://docs.phpunit.de/en/9.6/). It provides a lot of fundamental concepts that we build on in this documentation. -## Running Tests +## Running tests In order to run tests, you need to install Silverstripe CMS using [Composer](/getting-started/composer), which will pull in the required development dependencies to run tests. Tests are run from the commandline, in your webroot folder: - * `vendor/bin/phpunit`: Runs all tests (as defined by `phpunit.xml`) - * `vendor/bin/phpunit vendor/silverstripe/framework/tests/`: Run all tests of a specific module - * `vendor/bin/phpunit vendor/silverstripe/framework/tests/filesystem`: Run specific tests within a specific module - * `vendor/bin/phpunit vendor/silverstripe/framework/tests/filesystem/FolderTest.php`: Run a specific test - * `vendor/bin/phpunit vendor/silverstripe/framework/tests '' flush=1`: Run tests with optional request parameters (note the empty second argument) +- `vendor/bin/phpunit`: Runs all tests (as defined by `phpunit.xml`) +- `vendor/bin/phpunit vendor/silverstripe/framework/tests/`: Run all tests of a specific module +- `vendor/bin/phpunit vendor/silverstripe/framework/tests/filesystem`: Run specific tests within a specific module +- `vendor/bin/phpunit vendor/silverstripe/framework/tests/filesystem/FolderTest.php`: Run a specific test +- `vendor/bin/phpunit vendor/silverstripe/framework/tests '' flush=1`: Run tests with optional request parameters (note the empty second argument) Check the PHPUnit manual for all available [command line arguments](https://docs.phpunit.de/en/9.6/textui.html). -On Linux or OSX, you can avoid typing the full path on every invocation by adding `vendor/bin` +On Linux or OSX, you can avoid typing the full path on every invocation by adding `vendor/bin` to your `$PATH` definition in the shell profile (usually `~/.profile`): `PATH=./vendor/bin:$PATH` ## Caching @@ -46,7 +46,7 @@ In order to flush the cache, use the `flush=1` CLI parameter: vendor/bin/phpunit vendor/silverstripe/framework/tests '' flush=1 ``` -## Generating a Coverage Report +## Generating a coverage report PHPUnit can generate a code coverage report ([docs](https://docs.phpunit.de/en/9.6/code-coverage-analysis.html)) which shows you how much of your logic is executed by your tests. This is very useful to determine gaps in tests. @@ -57,24 +57,25 @@ vendor/bin/phpunit --coverage-html To view the report, open the `index.html` in `` in a web browser. -Typically, only your own custom PHP code in your project should be regarded when producing these reports. To exclude +Typically, only your own custom PHP code in your project should be regarded when producing these reports. To exclude some `thirdparty/` directories add the following to the `phpunit.xml` configuration file. + ```xml - - vendor/silverstripe/framework/dev/ - vendor/silverstripe/framework/thirdparty/ - vendor/silverstripe/cms/thirdparty/ - - - app/thirdparty/ - + + vendor/silverstripe/framework/dev/ + vendor/silverstripe/framework/thirdparty/ + vendor/silverstripe/cms/thirdparty/ + + + app/thirdparty/ + ``` ## Configuration -The `phpunit` executable can be configured by [command line arguments](https://docs.phpunit.de/en/9.6/textui.html) +The `phpunit` executable can be configured by [command line arguments](https://docs.phpunit.de/en/9.6/textui.html) or through an XML file. File-based configuration has the advantage of enforcing certain rules across test executions (e.g. excluding files from code coverage reports), and of course this information can be version controlled and shared with other team members. @@ -87,12 +88,12 @@ There's nothing stopping you from creating multiple XML files (see the `--config [PHPUnit documentation](https://docs.phpunit.de/en/9.6/textui.html)). For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below). -### Database Permissions +### Database permissions Silverstripe CMS tests create their own temporary database on every execution. Because of this the database user in your config file should have the appropriate permissions to create new databases on your server, otherwise tests will not run. -## Writing Tests +## Writing tests Tests are written by creating subclasses of [SapphireTest](api:SilverStripe\Dev\SapphireTest). You should put tests for your site in the `app/tests` directory. If you are writing tests for a module, put them in the `tests/` directory of your module (in `vendor/`). @@ -103,11 +104,11 @@ application class, with "Test" as a suffix. For instance, we have all the tests You will generally write two different kinds of test classes. -* **Unit Test:** Test the behaviour of one of your DataObjects. -* **Functional Test:** Test the behaviour of one of your controllers. +- **Unit Test:** Test the behaviour of one of your DataObjects. +- **Functional Test:** Test the behaviour of one of your controllers. Tutorials and recipes for creating tests using the Silverstripe CMS: -* [Creating a Silverstripe CMS test](how_tos/write_a_sapphiretest): Writing tests to check core data objects -* [Creating a functional test](how_tos/write_a_functionaltest): An overview of functional tests and how to write a functional test -* [Testing Outgoing Email](how_tos/testing_email): An overview of the built-in email testing code +- [Creating a Silverstripe CMS test](how_tos/write_a_sapphiretest): Writing tests to check core data objects +- [Creating a functional test](how_tos/write_a_functionaltest): An overview of functional tests and how to write a functional test +- [Testing Outgoing Email](how_tos/testing_email): An overview of the built-in email testing code diff --git a/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md b/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md index 8dba87286..becdbe130 100644 --- a/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md +++ b/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md @@ -4,33 +4,32 @@ summary: Configure your Silverstripe CMS environment to define how your web appl icon: exclamation-circle --- -# Environment Types +# Environment types Silverstripe CMS knows three different environment types (or "modes"). Each of the modes gives you different tools -and behaviors. The environment is managed by the `SS_ENVIRONMENT_TYPE` variable through an +and behaviors. The environment is managed by the `SS_ENVIRONMENT_TYPE` variable through an [environment configuration file](../../getting_started/environment_management). The three environment types you can set are `dev`, `test` and `live`. -### Dev +## Dev When developing your websites, adding page types or installing modules you should run your site in `dev`. In this mode -you will see full error back traces and view the development tools without having to be logged in as an administrator +you will see full error back traces and view the development tools without having to be logged in as an administrator user. [alert] -**dev mode should not be enabled long term on live sites for security reasons**. In dev mode by outputting back traces -of function calls a hacker can gain information about your environment (including passwords) so you should use dev mode +**dev mode should not be enabled long term on live sites for security reasons**. In dev mode by outputting back traces +of function calls a hacker can gain information about your environment (including passwords) so you should use dev mode on a public server very carefully. [/alert] -### Test Mode +## Test mode Test mode is designed for staging environments or other private collaboration sites before deploying a site live. -In this mode error messages are hidden from the user and Silverstripe CMS includes [BasicAuth](api:SilverStripe\Security\BasicAuth) integration if you +In this mode error messages are hidden from the user and Silverstripe CMS includes [BasicAuth](api:SilverStripe\Security\BasicAuth) integration if you want to password protect the site. You can enable that by adding this to your `app/_config/app.yml` file: - ```yml --- Only: @@ -47,7 +46,7 @@ Consider using additional authentication and authorisation measures to secure ac When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file -### Live Mode +## Live mode All error messages are suppressed from the user and the application is in it's most *secure* state. @@ -55,14 +54,12 @@ All error messages are suppressed from the user and the application is in it's m Live sites should always run in live mode. You should not run production websites in dev mode. [/alert] - -## Checking Environment Type +## Checking environment type You can check for the current environment type in [config files](../configuration) through the `environment` variant. -**app/_config/app.yml** - ```yml +# app/_config/app.yml --- Only: environment: 'live' @@ -76,7 +73,8 @@ Only: MyClass: myvar: test_value ``` -Checking for what environment you're running in can also be done in PHP. Your application code may disable or enable + +Checking for what environment you're running in can also be done in PHP. Your application code may disable or enable certain functionality depending on the environment type. ```php @@ -91,5 +89,6 @@ if (Director::isLive()) { } ``` -## Related Lessons -* [Advanced environment configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) +## Related lessons + +- [Advanced environment configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) diff --git a/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md b/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md index 5fa0969f2..187c03daf 100644 --- a/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md +++ b/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md @@ -4,7 +4,7 @@ summary: Trap, fire and report diagnostic logs, user exceptions, warnings and er icon: exclamation-circle --- -# Logging and Error Handling +# Logging and error handling Silverstripe CMS uses Monolog for both error handling and logging. It comes with two default configurations: one for logging, and another for core error handling. The core error handling implementation also comes with two default @@ -17,14 +17,14 @@ There are a range of monolog handlers available, both in the core package and in [Monolog documentation](https://github.com/Seldaek/monolog/blob/main/doc/01-usage.md) for more information. [/info] -## Raising errors and logging diagnostic information. +## Raising errors and logging diagnostic information For general purpose logging, you can use the Logger directly. The Logger is a PSR-3 compatible LoggerInterface and can be accessed via the `Injector`: ```php -use SilverStripe\Core\Injector\Injector; use Psr\Log\LoggerInterface; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Security\Security; Injector::inst()->get(LoggerInterface::class)->info('User has logged in: ID #' . Security::getCurrentUser()->ID); @@ -40,22 +40,31 @@ where appropriate. As with the default Logger implementation these will not halt to the PHP error log. ```php -public function delete() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyObject extends DataObject { - if ($this->alreadyDelete) { - user_error("Delete called on already deleted object", E_USER_NOTICE); - return; - } // ... -} - -public function getRelatedObject() -{ - if (!$this->RelatedObjectID) { - user_error("Can't find a related object", E_USER_WARNING); - return; + + public function delete() + { + if ($this->alreadyDelete) { + user_error('Delete called on already deleted object', E_USER_NOTICE); + return; + } + // ... + } + + public function getRelatedObject() + { + if (!$this->RelatedObjectID) { + user_error("Can't find a related object", E_USER_WARNING); + return; + } + // ... } - // ... } ``` @@ -63,7 +72,7 @@ For errors that should halt execution, you should use Exceptions. Normally, Exce but they can be caught with a try/catch clause. ```php -throw new \LogicException("Query failed: " . $sql); +throw new LogicException('Query failed: ' . $sql); ``` ### Accessing the logger via dependency injection @@ -74,6 +83,8 @@ approach is to use dependency injection to pass the logger in for you. The [Inje can help with this. The most straightforward is to specify a `dependencies` config setting, like this: ```php +namespace App\Control; + use Psr\Log\LoggerInterface; use SilverStripe\Control\Controller; @@ -92,10 +103,10 @@ class MyController extends Controller protected function init() { - $this->logger->debug("MyController::init() called"); + $this->logger->debug('MyController::init() called'); parent::init(); } - + /** * @param LoggerInterface $logger * @return $this @@ -111,15 +122,15 @@ class MyController extends Controller In other contexts, such as testing or batch processing, logger can be set to a different value by the code calling MyController. -### Error Levels +### Error levels -* **E_USER_WARNING:** Err on the side of over-reporting warnings. Throwing warnings provides a means of ensuring that +- **E_USER_WARNING:** Err on the side of over-reporting warnings. Throwing warnings provides a means of ensuring that developers know: - * Deprecated functions / usage patterns - * Strange data formats - * Things that will prevent an internal function from continuing. Throw a warning and return null. + - Deprecated functions / usage patterns + - Strange data formats + - Things that will prevent an internal function from continuing. Throw a warning and return null. -* **E_USER_ERROR:** Throwing one of these errors is going to take down the production site. So you should only throw +- **E_USER_ERROR:** Throwing one of these errors is going to take down the production site. So you should only throw E_USER_ERROR if it's going to be **dangerous** or **impossible** to continue with the request. Note that it is preferable to now throw exceptions instead of `E_USER_ERROR`. @@ -133,9 +144,9 @@ for you to try. To send emails, you can use Monolog's `NativeMailerHandler`, like this: -```yaml +```yml SilverStripe\Core\Injector\Injector: - Psr\Log\LoggerInterface: + Psr\Log\LoggerInterface: calls: MailHandler: [ pushHandler, [ '%$MailHandler' ] ] MailHandler: @@ -160,9 +171,9 @@ The calls key, `MailHandler`, can be anything you like: its main purpose is to l To log to a file, you can use Monolog's `StreamHandler`, like this: -```yaml +```yml SilverStripe\Core\Injector\Injector: - Psr\Log\LoggerInterface: + Psr\Log\LoggerInterface: calls: LogFileHandler: [ pushHandler, [ '%$LogFileHandler' ] ] LogFileHandler: @@ -173,17 +184,17 @@ SilverStripe\Core\Injector\Injector: ``` [warning] -The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a _relative_ path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: +The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a *relative* path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: -```sh +```bash SS_ERROR_LOG="./silverstripe.log" ``` -You don't need any of the yaml configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and yaml configuration if you want to configure multiple error log files. +You don't need any of the YAML configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and YAML configuration if you want to configure multiple error log files. [/warning] [notice] -You will need to make sure the user running the php process has write access to the log file, wherever you choose to put it. +You will need to make sure the user running the PHP process has write access to the log file, wherever you choose to put it. [/notice] The `info` argument provides the minimum level to start logging at. @@ -193,7 +204,7 @@ The `info` argument provides the minimum level to start logging at. You can disable a handler by removing its pushHandlers call from the calls option of the Logger service definition. The handler key of the default handler is `pushDisplayErrorHandler`, so you can disable it like this: -```yaml +```yml SilverStripe\Core\Injector\Injector: Psr\Log\LoggerInterface.errorhandler: calls: @@ -206,7 +217,7 @@ In order to set different logging configuration on different environment types, configuration features that the config system providers. For example, here we have different configuration for dev and non-dev. -```yaml +```yml --- Name: dev-errors Only: @@ -234,15 +245,15 @@ SilverStripe\Core\Injector\Injector: calls: # Save system logs to file pushFileLogHandler: [ pushHandler, [ '%$LogFileHandler' ]] - + # Core error handler for system use Psr\Log\LoggerInterface.errorhandler: calls: # Save errors to file pushFileLogHandler: [ pushHandler, [ '%$LogFileHandler' ]] - # Format and display errors in the browser/CLI + # Format and display errors in the browser/CLI pushMyDisplayErrorHandler: [ pushHandler, [ '%$DisplayErrorHandler' ]] - + # Custom handler to log to a file LogFileHandler: class: Monolog\Handler\StreamHandler @@ -252,7 +263,7 @@ SilverStripe\Core\Injector\Injector: properties: Formatter: '%$Monolog\Formatter\HtmlFormatter' ContentType: text/html - + # Handler for displaying errors in the browser or CLI DisplayErrorHandler: class: SilverStripe\Logging\HTTPOutputHandler @@ -260,7 +271,7 @@ SilverStripe\Core\Injector\Injector: - "error" properties: Formatter: '%$SilverStripe\Logging\DebugViewFriendlyErrorFormatter' - + # Configuration for the "friendly" error formatter SilverStripe\Logging\DebugViewFriendlyErrorFormatter: class: SilverStripe\Logging\DebugViewFriendlyErrorFormatter @@ -287,7 +298,7 @@ Monolog comes by default with Silverstripe CMS, but you may use another PSR-3 co set the `SilverStripe\Core\Injector\Injector.Monolog\Logger` configuration parameter, providing a new injector definition. For example: -```yaml +```yml SilverStripe\Core\Injector\Injector: SilverStripe\Logging\ErrorHandler: class: Logging\Logger @@ -301,19 +312,19 @@ be ignored. ### Replacing the error handler The Injector service `SilverStripe\Logging\ErrorHandler` is responsible for initialising the error handler. By default -it: +it: + +- Create a `SilverStripe\Logging\MonologErrorHandler` object. +- Attach the registered service `Psr\Log\LoggerInterface` to it, to start the error handler. - * Create a `SilverStripe\Logging\MonologErrorHandler` object. - * Attach the registered service `Psr\Log\LoggerInterface` to it, to start the error handler. - -Core.php will call `start()` on this method, to start the error handler. +`Core.php` will call `start()` on this method, to start the error handler. This error handler is flexible enough to work with any PSR-3 logging implementation, but sometimes you will want to use another. To replace this, you should registered a new service, `ErrorHandlerLoader`. For example: -```yaml +```yml SilverStripe\Core\Injector\Injector: - SilverStripe\Logging\ErrorHandler: + SilverStripe\Logging\ErrorHandler: class: MyApp\CustomErrorHandlerLoader ``` @@ -343,5 +354,6 @@ You should include any functions or methods here which have arguments that may b module that other developers may use, it is best practice to include this configuration in the module. Developers should not be expected to scan every Silverstripe module they use and add those declarations in their project configuration. -## Related Lessons -* [Advanced environment configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) +## Related lessons + +- [Advanced environment configuration](https://www.silverstripe.org/learn/lessons/v4/advanced-environment-configuration-1) diff --git a/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md b/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md index 53dde3a4f..f74967450 100644 --- a/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md +++ b/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md @@ -2,15 +2,15 @@ title: URL Variable tools summary: Useful debugging tools you can use right in the browser --- -# URL Variable Tools +# URL variable tools ## Introduction This page lists a number of "page options" , "rendering tools" or "special URL variables" that you can use to debug your -Silverstripe CMS applications. These are consumed in PHP using the $_REQUEST or $_GET superglobals throughout the +Silverstripe CMS applications. These are consumed in PHP using the $_REQUEST or $_GET superglobals throughout the Silverstripe CMS core. -## Debug Toolbar +## Debug toolbar The easiest way to debug Silverstripe CMS is through the [lekoala/silverstripe-debugbar](https://github.com/lekoala/silverstripe-debugbar) module. @@ -21,60 +21,60 @@ session variables, used templates and much more. ## Templates | URL Variable | Values | Description | - | ------------ | ------ | ----------- | + | ------------ | ------ | ----------- | | flush | 1 | Clears out all caches. Used mainly during development, e.g. when adding new classes or templates. Requires "dev" mode or ADMIN login | | showtemplate | 1 | Show the compiled version of all the templates used, including line numbers. Good when you have a syntax error in a template. Cannot be used on a Live site without **isDev**. | -## General Testing +## General testing - | URL Variable | Values | Description | - | ------------ | ------ | ----------- | - | isDev | 1 | Put the site into [development mode](../), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. | - | isTest | 1 | See above. | + | URL Variable | Values | Description | + | ------------ | ------ | ----------- | + | isDev | 1 | Put the site into [development mode](../), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. | + | isTest | 1 | See above. | | debug | 1 | Show a collection of debugging information about the director / controller operation | | debug_request | 1 | Show all steps of the request from initial [HTTPRequest](api:SilverStripe\Control\HTTPRequest) to [Controller](api:SilverStripe\Control\Controller) to Template Rendering | - | execmetric | 1 | Display the execution time and peak memory usage for the request | + | execmetric | 1 | Display the execution time and peak memory usage for the request | -## Classes and Objects +## Classes and objects - | URL Variable | Values | Description | + | URL Variable | Values | Description | | ------------ | ------ | ----------- | - | debugfailover | 1 | Shows failover methods from classes extended | + | debugfailover | 1 | Shows failover methods from classes extended | ## Database - | URL Variable | Values | Description | - | ------------ | ------------------ | ----------- | - | showqueries | 1 | inline | List all SQL queries executed, the `inline` option will do a fudge replacement of parameterised queries | + | URL Variable | Values | Description | + | ------------ | ------------------ | ----------- | + | showqueries | 1 | inline | List all SQL queries executed, the `inline` option will do a fudge replacement of parameterised queries | | showqueries | 1 | backtrace | List all SQL queries executed, the `backtrace` option will do a fudge replacement of parameterised queries *and* show a backtrace of every query | - | previewwrite | 1 | List all insert / update SQL queries, and **don't** execute them. Useful for previewing writes to the database. | + | previewwrite | 1 | List all insert / update SQL queries, and **don't** execute them. Useful for previewing writes to the database. | -## Security Redirects +## Security redirects You can set an URL to redirect back to after a [Security](/developer_guides/security) action. See the section on [URL Redirections](/developer_guides/controllers/redirection) for more information and examples. - | URL Variable | Values | Description | - | ------------ | ------ | ----------- | - | BackURL | URL | Set to a relative URL string to use once Security Action is complete | + | URL Variable | Values | Description | + | ------------ | ------ | ----------- | + | BackURL | URL | Set to a relative URL string to use once Security Action is complete | -## Building and Publishing URLs +## Building and publishing URLs - | Site URL | Action | - | --------------------------------------------- | ------ | - | https://www.example.com/dev/build** | Rebuild the entire database and manifest, see below for additional URL Variables | - | https://www.example.com/admin/pages/publishall/** | Publish all pages on the site. Only works reliably on smaller sites. | + | Site URL | Action | + | --------------------------------------------- | ------ | + | `https://www.example.com/dev/build**` | Rebuild the entire database and manifest, see below for additional URL Variables | + | `https://www.example.com/admin/pages/publishall/**` | Publish all pages on the site. Only works reliably on smaller sites. | -### /dev/build +### /dev/build - | URL Variable | Values | Description | - | ------------ | ------ | ----------- | - | quiet | 1 | Don't show messages during build | - | dont_populate | 1 | Don't run **requireDefaultRecords()** on the models when building. This will build the table but not insert any records | + | URL Variable | Values | Description | + | ------------ | ------ | ----------- | + | quiet | 1 | Don't show messages during build | + | dont_populate | 1 | Don't run **requireDefaultRecords()** on the models when building. This will build the table but not insert any records | ## Config diagnostic URLs - | Site URL | Action | - | --------------------------------------------- | ------ | - | https://www.example.com/dev/config** | Output a simplified representation properties in the `Config` manifest | - | https://www.example.com/dev/config/audit** | Audit `Config` properties and display any with missing PHP definitions | + | Site URL | Action | + | --------------------------------------------- | ------ | + | `https://www.example.com/dev/config**` | Output a simplified representation properties in the `Config` manifest | + | `https://www.example.com/dev/config/audit**` | Audit `Config` properties and display any with missing PHP definitions | diff --git a/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md b/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md index fc226be72..bd6f0af6f 100644 --- a/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md +++ b/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md @@ -8,9 +8,9 @@ icon: bug ## Source code comments -If there is a problem with the rendered html your page is outputting you may need -to track down a template or two. The template engine can help you along by displaying -source code comments indicating which template is responsible for rendering each +If there is a problem with the rendered html your page is outputting you may need +to track down a template or two. The template engine can help you along by displaying +source code comments indicating which template is responsible for rendering each block of html on your page. ```yml diff --git a/en/02_Developer_Guides/07_Debugging/index.md b/en/02_Developer_Guides/07_Debugging/index.md index 0c4bbc4dd..05b45f4a5 100644 --- a/en/02_Developer_Guides/07_Debugging/index.md +++ b/en/02_Developer_Guides/07_Debugging/index.md @@ -16,13 +16,13 @@ built-in helpers for dealing with application errors. See the [Profiling](../performance/profiling) documentation for more information on profiling Silverstripe CMS to track down bottle-necks and identify slow moving parts of your application chain. -## Debugging Utilities +## Debugging utilities The [Debug](api:SilverStripe\Dev\Debug) class contains a number of static utility methods for more advanced debugging. ```php -use SilverStripe\Dev\Debug; use SilverStripe\Dev\Backtrace; +use SilverStripe\Dev\Debug; Debug::show($myVariable); // similar to print_r($myVariable) but shows it in a more useful format. @@ -34,7 +34,7 @@ Backtrace::backtrace(); // prints a calls-stack ``` -## API Documentation +## API documentation -* [Backtrace](api:SilverStripe\Dev\Backtrace) -* [Debug](api:SilverStripe\Dev\Debug) +- [Backtrace](api:SilverStripe\Dev\Backtrace) +- [Debug](api:SilverStripe\Dev\Debug) diff --git a/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index d981e7441..557b719af 100644 --- a/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -4,11 +4,10 @@ summary: Cache Silverstripe CMS templates to reduce database queries. icon: tachometer-alt --- -# Partial Caching +# Partial caching [Partial template caching](../templates/partial_template_caching) is a feature that allows caching of rendered portions a template. - ## Cache block conditionals Use conditions whenever possible. The cache tag supports defining conditions via either `if` or `unless` keyword. @@ -19,8 +18,9 @@ Avoid performing heavy computations in conditionals, as they are evaluated for e [/warning] If you cache without conditions: - - your cache backend will always be queried for the cache block (on every template render) - - your cache may be cluttered with heaps of redundant and useless data (especially the default filesystem backend) + +- your cache backend will always be queried for the cache block (on every template render) +- your cache may be cluttered with heaps of redundant and useless data (especially the default filesystem backend) As an example, if you use `$DataObject->ID` as a key for the block, consider adding a condition that ID is greater than zero: @@ -35,7 +35,6 @@ To cache the contents of a page for all anonymous users, but dynamically calcula <% cached unless $CurrentUser %> ``` - ## Aggregates Sometimes you may want to invalidate cache when any object in a set changes, or when objects in a relationship @@ -43,15 +42,16 @@ change. To do this, you may use [DataList](api:SilverStripe\ORM\DataList) aggreg These perform SQL aggregate queries on sets of [DataObject](api:SilverStripe\ORM\DataObject)s. Here are some useful methods of the [DataList](api:SilverStripe\ORM\DataList) class: - - `int count()` : Return the number of items in this DataList - - `mixed max(string $fieldName)` : Return the maximum value of the given field in this DataList - - `mixed min(string $fieldName)` : Return the minimum value of the given field in this DataList - - `mixed avg(string $fieldName)` : Return the average value of the given field in this DataList - - `mixed sum(string $fieldName)` : Return the sum of the values of the given field in this DataList + +- `int count()` : Return the number of items in this DataList +- `mixed max(string $fieldName)` : Return the maximum value of the given field in this DataList +- `mixed min(string $fieldName)` : Return the minimum value of the given field in this DataList +- `mixed avg(string $fieldName)` : Return the average value of the given field in this DataList +- `mixed sum(string $fieldName)` : Return the sum of the values of the given field in this DataList To construct a `DataList` over a `DataObject`, we have a global template variable called `$List`. -For example, if we have a menu, we may want that menu to update whenever _any_ page is edited, but would like to cache it +For example, if we have a menu, we may want that menu to update whenever *any* page is edited, but would like to cache it otherwise. By using aggregates, we do that like this: ```ss @@ -102,7 +102,7 @@ Parent title is: $Me.Parent.Title ``` `Version 1` always generates two heavy aggregating SQL queries for the database on every -template render. +template render. `Version 2` always generates a single and more performant SQL query fetching the record by its Primary Key. [/warning] @@ -114,7 +114,6 @@ unless you move it out into a separate [Object Caching](../templates/caching/#object-caching) only works for single variables and not for chained expressions. [/warning] - ## Purposely stale data In some situations it's more important to be fast than to always be showing the latest data. By constructing the cache @@ -123,7 +122,6 @@ data updates. For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do - ```ss <% cached 'blogstatistics', $Blog.ID %> ``` @@ -131,37 +129,42 @@ For instance, if we show some blog statistics, but are happy having them be slig which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is configurable only on a site-wide basis), you could add a special function to your controller: - ```php -public function BlogStatisticsCounter() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyObject extends DataObject { - return (int)(time() / 60 / 5); // Returns a new number every five minutes + // ... + + public function getBlogStatisticsCount() + { + // Returns a new number every five minutes + return (int)(time() / 60 / 5); + } } ``` - and then use it in the cache key - ```ss -<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %> +<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCount %> ``` - ## Cache backend The template engine uses [Injector](../extending/injector) service `Psr\SimpleCache\CacheInterface.cacheblock` as -caching backend. The default definition of that service is very conservative and relies on the server filesystem. +caching backend. The default definition of that service is very conservative and relies on the server filesystem. This is the most common denominator for most of the applications out there. However, this is not the most robust neither performant cache implementation. If you have a better solution -available on your platform, you should consider tuning that setting for your application. +available on your platform, you should consider tuning that setting for your application. All you need to do to swap the cache backend for partial template cache blocks is to redefine this service for the Injector. Here's an example of how it could be done: ```yml # app/_config/cache.yml - --- Name: app-cache After: diff --git a/en/02_Developer_Guides/08_Performance/01_Caching.md b/en/02_Developer_Guides/08_Performance/01_Caching.md index 69722e98d..3325a8078 100644 --- a/en/02_Developer_Guides/08_Performance/01_Caching.md +++ b/en/02_Developer_Guides/08_Performance/01_Caching.md @@ -11,19 +11,19 @@ The framework uses caches to store infrequently changing values. By default, the storage mechanism chooses the most performant adapter available (PHP opcache, APC, or filesystem). Other cache backends can be configured. -The most common caches are manifests of various resources: +The most common caches are manifests of various resources: - * PHP class locations ([ClassManifest](api:SilverStripe\Core\Manifest\ClassManifest)) - * Configuration settings from YAML files ([CachedConfigCollection](api:SilverStripe\Config\Collections\CachedConfigCollection)) - * Language files ([i18n](api:SilverStripe\i18n\i18n)) +- PHP class locations ([ClassManifest](api:SilverStripe\Core\Manifest\ClassManifest)) +- Configuration settings from YAML files ([CachedConfigCollection](api:SilverStripe\Config\Collections\CachedConfigCollection)) +- Language files ([i18n](api:SilverStripe\i18n\i18n)) Flushing the various manifests is performed through a GET parameter (`flush=1`). Since this action requires more server resources than normal requests, executing the action is limited to the following cases when performed via a web request: - * The [environment](/getting_started/environment_management) is in "dev mode" - * A user is logged in with ADMIN permissions - * An error occurs during startup +- The [environment](/getting_started/environment_management) is in "dev mode" +- A user is logged in with ADMIN permissions +- An error occurs during startup Caution: Not all caches are cleared through `flush=1`. While cache objects can expire, when using filesystem caching the files are not actively pruned. @@ -39,8 +39,7 @@ Note that this library describes usage of [PSR-6](https://www.php-fig.org/psr/ps though Silverstripe wraps these in a PSR-16 interface using the [Psr16Cache](https://github.com/symfony/cache/blob/6.1/Psr16Cache.php) class. Cache objects are configured via YAML -and Silverstripe CMS's [dependency injection](/developer_guides/extending/injector) system. - +and Silverstripe CMS's [dependency injection](/developer_guides/extending/injector) system. ```yml SilverStripe\Core\Injector\Injector: @@ -53,14 +52,13 @@ SilverStripe\Core\Injector\Injector: [alert] Please note that if you have the `silverstripe/versioned` module installed (automatically installed by the `silverstripe/cms` module), caches will automatically be segmented by current “stage”. This ensures that -any content written to the cache in the _draft_ reading mode isn’t accidentally exposed in the _live_ reading mode. +any content written to the cache in the *draft* reading mode isn’t accidentally exposed in the *live* reading mode. Please read the [versioned cache segmentation](#versioned-cache-segmentation) section for more information. [/alert] Cache objects are instantiated through a [CacheFactory](api:SilverStripe\Core\Cache\CacheFactory), which determines which cache adapter is used (see "Adapters" below for details). -This factory allows us you to globally define an adapter for all cache instances. - +This factory allows us you to globally define an adapter for all cache instances. ```php use Psr\SimpleCache\CacheInterface; @@ -72,17 +70,15 @@ $cache = Injector::inst()->get(CacheInterface::class . '.myCache'); Caches are namespaced, which might allow granular clearing of a particular cache without affecting others. In our example, the namespace is "myCache", expressed in the service name as `Psr\SimpleCache\CacheInterface.myCache`. We recommend the `::class` short-hand to compose the full service name. - + Clearing caches by namespace is dependent on the used adapter: While the `FilesystemAdapter` clears only the namespaced cache, a `MemcachedAdapter` adapter will clear all caches regardless of namespace, since the underlying memcached service doesn't support this. See "Invalidation" for alternative strategies. - ## Usage Cache objects follow the [PSR-16](https://www.php-fig.org/psr/psr-16/) class interface. - ```php use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; @@ -108,7 +104,6 @@ Caches can be invalidated in different ways. The easiest is to actively clear th entire cache. If the adapter supports namespaced cache clearing, this will only affect a subset of cache keys ("myCache" in this example): - ```php use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; @@ -121,7 +116,6 @@ $cache->clear(); You can also delete a single item based on it's cache key: - ```php use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; @@ -134,7 +128,6 @@ $cache->delete('myCacheKey'); Individual cache items can define a lifetime, after which the cached value is marked as expired: - ```php use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; @@ -142,7 +135,8 @@ use SilverStripe\Core\Injector\Injector; $cache = Injector::inst()->get(CacheInterface::class . '.myCache'); // set a cache item with an expiry -$cache->set('myCacheKey', 'myValue', 300); // cache for 300 seconds +// cache for 300 seconds +$cache->set('myCacheKey', 'myValue', 300); ``` If a lifetime isn't defined on the `set()` call, it'll use the adapter default. @@ -152,7 +146,6 @@ You can also set your lifetime to `0`, which means they won't expire. Since many adapters don't have a way to actively remove expired caches, you need to be careful with resources here (e.g. filesystem space). - ```yml --- Name: my-project-cache @@ -171,7 +164,6 @@ The following example caches a member's group names, and automatically creates a new cache key when any group is edited. Depending on the used adapter, old cache keys will be garbage collected as the cache fills up. - ```php use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; @@ -192,17 +184,17 @@ interface. Use this interface to trigger `clear()` on your caches. Silverstripe CMS tries to identify the most performant cache available on your system through the [DefaultCacheFactory](api:SilverStripe\Core\Cache\DefaultCacheFactory) implementation: - * `PhpFilesAdapter` (PHP with [opcache](https://php.net/manual/en/book.opcache.php) enabled). - This cache has relatively low [memory defaults](https://php.net/manual/en/opcache.configuration.php#ini.opcache.memory-consumption). - We recommend increasing it for large applications, or enabling the - [file_cache fallback](https://php.net/manual/en/opcache.configuration.php#ini.opcache.file-cache) - * `ApcuAdapter` (requires APC) with a `FilesystemAdapter` fallback (for larger cache volumes) - * `FilesystemAdapter` if none of the above is available - +- `PhpFilesAdapter` (PHP with [opcache](https://php.net/manual/en/book.opcache.php) enabled). + This cache has relatively low [memory defaults](https://php.net/manual/en/opcache.configuration.php#ini.opcache.memory-consumption). + We recommend increasing it for large applications, or enabling the + [file_cache fallback](https://php.net/manual/en/opcache.configuration.php#ini.opcache.file-cache) +- `ApcuAdapter` (requires APC) with a `FilesystemAdapter` fallback (for larger cache volumes) +- `FilesystemAdapter` if none of the above is available + The library supports various [cache adapters](https://github.com/symfony/cache/tree/6.1/Adapter) which can provide better performance, particularly in multi-server environments with shared caches like Memcached. -Since we're using dependency injection to create caches, +Since we're using dependency injection to create caches, you need to define a factory for a particular adapter, following the `SilverStripe\Core\Cache\CacheFactory` interface. Different adapters will require different constructor arguments. @@ -213,7 +205,6 @@ Example: Configure core caches to use [memcached](https://www.danga.com/memcache which requires the [memcached PHP extension](https://php.net/memcached), and takes a `MemcachedClient` instance as a constructor argument. - ```yml --- After: '#versionedcache' @@ -245,31 +236,35 @@ $cache = Injector::inst()->get(CacheInterface::class . '.myapp'); Versioned::set_stage(Versioned::DRAFT); $cache->set('my_key', 'Some draft content. Not for public viewing yet.'); Versioned::set_stage(Versioned::LIVE); -$cache->get('my_key'); // 'Some draft content. Not for public viewing yet' +// 'Some draft content. Not for public viewing yet' +$cache->get('my_key'); // After: $cache = Injector::inst()->get(CacheInterface::class . '.myapp'); Versioned::set_stage(Versioned::DRAFT); $cache->set('my_key', 'Some draft content. Not for public viewing yet.'); Versioned::set_stage(Versioned::LIVE); -$cache->get('my_key'); // null +// null +$cache->get('my_key'); ``` + Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-container` argument. -```yaml +```yml SilverStripe\Core\Injector\Injector: Psr\SimpleCache\CacheInterface.myapp: factory: SilverStripe\Core\Cache\CacheFactory constructor: namespace: "MyInsensitiveData" - disable-container: true + disable-container: true ``` -## Additional Caches +## Additional caches Unfortunately not all caches are configurable via cache adapters. - * [SSViewer](api:SilverStripe\View\SSViewer) writes compiled templates as PHP files to the filesystem +- [SSViewer](api:SilverStripe\View\SSViewer) writes compiled templates as PHP files to the filesystem (in order to achieve opcode caching on `include()` calls) - * [i18n](api:SilverStripe\i18n\i18n) uses `Symfony\Component\Config\ConfigCacheFactoryInterface` (filesystem-based) + +- [i18n](api:SilverStripe\i18n\i18n) uses `Symfony\Component\Config\ConfigCacheFactoryInterface` (filesystem-based) diff --git a/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md b/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md index be06766b4..eec795a12 100644 --- a/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md +++ b/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md @@ -4,9 +4,7 @@ summary: Set the correct HTTP cache headers for your responses. icon: tachometer-alt --- -# HTTP Cache Headers - -## Overview +# HTTP cache headers By default, Silverstripe CMS sends headers which signal to HTTP caches that the response should be not considered cacheable. @@ -18,7 +16,7 @@ Getting it wrong can accidentally expose draft pages or other protected content. The [Google Web Fundamentals](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private) are a great way to learn about HTTP caching. -## Cache Control Headers +## Cache control headers ### Overview @@ -41,11 +39,11 @@ since there are too many variations under which output could be considered priva (e.g. a custom "approval" flag on a comment object). It is up to the developer to ensure caching is used appropriately there. -The [api:SilverStripe\Control\Middleware\HTTPCacheControlMiddleware] class replaces +The [`HTTPCacheControlMiddleware`](api:SilverStripe\Control\Middleware\HTTPCacheControlMiddleware) class replaces (deprecated) caching methods in the `HTTP` helper class. It comes with methods which let developers safely interact with the `Cache-Control` header. -### disableCache() +### DisableCache() Simple way to set cache control header to a non-cacheable state. Use this method over `privateCache()` if you are unsure about caching details. @@ -57,7 +55,7 @@ the others are added under [recommendation from Mozilla](https://developer.mozil Does not set `private` directive, use `privateCache()` if this is explicitly required ([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private)) -### enableCache() +### EnableCache() Simple way to set cache control header to a cacheable state. Use this method over `publicCache()` if you are unsure about caching details. @@ -68,55 +66,57 @@ Use alongside `setMaxAge()` to activate caching. Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required ([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private)) -### privateCache() +### PrivateCache() Advanced way to set cache control header to a non-cacheable state. Indicates that the response is intended for a single user and must not be stored by a shared cache. A private cache (e.g. Web Browser) may store the response. Also removes `public` as this is a contradictory directive. -### publicCache() +### PublicCache() Advanced way to set cache control header to a cacheable state. -Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers) +Indicates that the response may be cached by any cache (e.g. CDNs, Proxies, Web browsers). Also removes `private` as this is a contradictory directive ### Priority - + Each of these highlevel methods has a boolean `$force` parameter which determines their application priority regardless of execution order. The priority order is as followed, sorted in descending order -(earlier items will overrule later items): +(earlier items will overrule later items): - * `disableCache($force=true)` - * `privateCache($force=true)` - * `publicCache($force=true)` - * `enableCache($force=true)` - * `disableCache()` - * `privateCache()` - * `publicCache()` - * `enableCache()` +- `disableCache($force=true)` +- `privateCache($force=true)` +- `publicCache($force=true)` +- `enableCache($force=true)` +- `disableCache()` +- `privateCache()` +- `publicCache()` +- `enableCache()` -## Cache Control Examples +## Cache control examples -### Global opt-in for page content +### Global opt-in for page content Enable caching for all page content (through `PageController`). ```php -enableCache() - ->setMaxAge(60); // 1 minute - - parent::init(); + public function init() + { + HTTPCacheControlMiddleware::singleton() + ->enableCache() + // 1 minute + ->setMaxAge(60); + + parent::init(); + } } } ``` @@ -133,33 +133,34 @@ you can disable caching either on a controller level (through `init()`) or for a particular action. ```php -disableCache(); - - return $this->myPrivateResponse(); + public function myprivateaction($request) + { + HTTPCacheControlMiddleware::singleton() + ->disableCache(); + + return $this->myPrivateResponse(); + } } } ``` Note: Silverstripe CMS will still override this preference when a session is active, a [CSRF token](/developer_guides/forms/form_security) token is present, -or draft content has been requested. +or draft content has been requested. -### Global opt-in, ignoring session (advanced) +### Global opt-in, ignoring session (advanced) This can be helpful in situations where forms are embedded on the website. Silverstripe CMS will still override this preference when draft content has been requested. CAUTION: This mode relies on a developer examining each execution path to ensure -that no session data is used to vary output. +that no session data is used to vary output. Use case: By default, forms include a [CSRF token](/developer_guides/forms/form_security) which starts a session with a value that's unique to the visitor, which makes the output uncacheable. @@ -169,30 +170,33 @@ and does not vary for this particular visitor. Forms can also contain submission when they're redisplayed after a validation error. ```php -enableCache($force=true) // DANGER ZONE - ->setMaxAge(60); // 1 minute - - parent::init(); + public function init() + { + HTTPCacheControlMiddleware::singleton() + // DANGER ZONE + ->enableCache($force = true) + // 1 minute + ->setMaxAge(60); + + parent::init(); + } } } ``` -## Max Age +## Max age The cache age determines the lifetime of your cache, in seconds. It only takes effect if you instruct the cache control that your response is cacheable in the first place -(via `enableCache()`, `publicCache()` or `privateCache()`), +(via `enableCache()`, `publicCache()` or `privateCache()`), or via modifying the `HTTP.cache_control` defaults). ```php @@ -204,10 +208,10 @@ HTTPCacheControlMiddleware::singleton() Note that `setMaxAge(0)` is NOT sufficient to disable caching in all cases, use `disableCache()` instead. -### Last Modified +### Last modified -Used to set the modification date to something more recent than the default. [api:DataObject::__construct] calls -[api:HTTP::register_modification_date(] whenever a record comes from the database ensuring the newest date is present. +Used to set the modification date to something more recent than the default. [`DataObject::__construct()`](api:SilverStripe\ORM\DataObject::__construct()) calls +[`HTTP::register_modification_date()`](api:SilverStripe\Control\HTTP::register_modification_date()) whenever a record comes from the database ensuring the newest date is present. ```php use SilverStripe\Control\HTTP; @@ -218,9 +222,9 @@ HTTP::register_modification_date('2014-10-10'); A `Vary` header tells caches which aspects of the response should be considered when calculating a cache key, usually in addition to the full URL. -By default, Silverstripe CMS will output a `Vary` header with the following content: +By default, Silverstripe CMS will output a `Vary` header with the following content: -``` +```text Vary: X-Forwarded-Protocol ``` @@ -236,7 +240,7 @@ then you should add `X-Requested-With` to the vary header. ## Testing -HTTP Cache headers are disabled in developer environments by default to prevent any confusion around content not updating. To enable HTTP Cache Headers in dev mode you can add the following in yml config. +HTTP Cache headers are disabled in developer environments by default to prevent any confusion around content not updating. To enable HTTP Cache Headers in dev mode you can add the following in YAML config. ```yml --- diff --git a/en/02_Developer_Guides/08_Performance/03_Profiling.md b/en/02_Developer_Guides/08_Performance/03_Profiling.md index 0acd838a5..4048240d7 100644 --- a/en/02_Developer_Guides/08_Performance/03_Profiling.md +++ b/en/02_Developer_Guides/08_Performance/03_Profiling.md @@ -6,11 +6,11 @@ icon: tachometer-alt # Profiling -Profiling is the best way to identify bottle necks and other slow moving parts of your application prime for -optimization. +Profiling is the best way to identify bottle necks and other slow moving parts of your application prime for +optimization. -Silverstripe CMS does not include any profiling tools out of the box, but we recommend the use of existing tools such as +Silverstripe CMS does not include any profiling tools out of the box, but we recommend the use of existing tools such as [XHProf](https://github.com/facebook/xhprof/) and [XDebug](https://xdebug.org/). -* [Profiling with XHProf](https://inviqa.com/blog/profiling-xhprof) -* [Profiling with xdebug](https://xdebug.org/docs/profiler) +- [Profiling with XHProf](https://inviqa.com/blog/profiling-xhprof) +- [Profiling with xdebug](https://xdebug.org/docs/profiler) diff --git a/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md b/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md index 7241926c5..114206fd0 100644 --- a/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md +++ b/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md @@ -3,18 +3,18 @@ title: Static Publishing summary: Export your web pages as static HTML and serve the web like it's 1999. --- -# Static Publishing +# Static publishing One of the best ways to get the top performance out of Silverstripe CMS is to bypass it completely. This saves on any loading -time, connecting to the database and formatting your templates. This is only appropriate approach on web pages that -have completely static content. +time, connecting to the database and formatting your templates. This is only appropriate approach on web pages that +have completely static content. [info] -If you want to cache part of a page, or your site has interactive elements such as forms, then +If you want to cache part of a page, or your site has interactive elements such as forms, then [Partial Caching](partial_caching) is more suitable. [/info] -By publishing the page as HTML it's possible to run Silverstripe CMS from behind a corporate firewall, on a low performance +By publishing the page as HTML it's possible to run Silverstripe CMS from behind a corporate firewall, on a low performance server or serve millions of hits an hour without expensive hardware. -This functionality is available through the [Static Publisher with Queue](https://github.com/silverstripe/silverstripe-staticpublishqueue) module. The module provides hooks for developers to generate static HTML files for the whole application or publish key pages (e.g a web applications home page) as HTML to reduce load on the server. +This functionality is available through the [Static Publisher with Queue](https://github.com/silverstripe/silverstripe-staticpublishqueue) module. The module provides hooks for developers to generate static HTML files for the whole application or publish key pages (e.g. a web applications home page) as HTML to reduce load on the server. diff --git a/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md b/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md index e6cf83c07..c493df40c 100644 --- a/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md +++ b/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md @@ -4,20 +4,20 @@ summary: Manage Silverstripe CMS's memory footprint and CPU usage. icon: tachometer-alt --- -# Resource Usage +# Resource usage -Silverstripe CMS tries to keep its resource usage within the documented limits +Silverstripe CMS tries to keep its resource usage within the documented limits (see the [server requirements](../../getting_started/server_requirements)). -These limits are defined through `memory_limit` and `max_execution_time` in the PHP configuration. They can be +These limits are defined through `memory_limit` and `max_execution_time` in the PHP configuration. They can be overwritten through `ini_set()`. [alert] Most shared hosting providers will have maximum values that can't be altered. [/alert] -For certain tasks like synchronizing a large `assets/` folder with all file and folder entries in the database, more -resources are required temporarily. In general, we recommend running resource intensive tasks through the +For certain tasks like synchronizing a large `assets/` folder with all file and folder entries in the database, more +resources are required temporarily. In general, we recommend running resource intensive tasks through the [command line](../cli), where configuration defaults for these settings are higher or even unlimited. [info] @@ -28,7 +28,7 @@ Silverstripe CMS can request more resources through `Environment::increaseMemory ```php use SilverStripe\Core\Environment; -public function myBigFunction() +public function myBigFunction() { Environment::increaseTimeLimitTo(400); } diff --git a/en/02_Developer_Guides/08_Performance/06_ORM.md b/en/02_Developer_Guides/08_Performance/06_ORM.md index c2722a9f8..d190234de 100644 --- a/en/02_Developer_Guides/08_Performance/06_ORM.md +++ b/en/02_Developer_Guides/08_Performance/06_ORM.md @@ -3,13 +3,13 @@ title: ORM Performance summary: Configuration and tips for improving ORM performance --- -# ORM Performance +# ORM performance ## Indexes You can define indexes for your ORM queries using the `$indexes` configuration property in your `DataObject` subclasses. See the [Indexes](/developer_guides/model/indexes) section for more information. -### TreeDropdownField SearchFilter configuration {#treedropdownfield} +### `TreeDropdownField` `SearchFilter` configuration {#treedropdownfield} The [`TreeDropdownField`](api:SilverStripe\Forms\TreeDropdownField) uses a [`PartialMatchFilter`](api:SilverStripe\ORM\Filters\PartialMatchFilter) by default to match against records. Indexes aren't effective when this filter is used, so you may find this field is slow with large datasets. diff --git a/en/02_Developer_Guides/08_Performance/index.md b/en/02_Developer_Guides/08_Performance/index.md index 6e8a8bc73..4c4608e79 100644 --- a/en/02_Developer_Guides/08_Performance/index.md +++ b/en/02_Developer_Guides/08_Performance/index.md @@ -5,12 +5,14 @@ introduction: Make your applications faster by learning how to write more scalab icon: tachometer-alt --- -The following guide describes the common ways to speed your Silverstripe CMS website up. The general rules for getting -the best performance out of Silverstripe CMS include running the latest versions of PHP alongside a +# Performance + +The following guide describes the common ways to speed your Silverstripe CMS website up. The general rules for getting +the best performance out of Silverstripe CMS include running the latest versions of PHP alongside a [opcode](https://en.wikipedia.org/wiki/Opcode) cache such as [OPcache](https://www.php.net/manual/en/book.opcache.php). If you're running shared hosting, make sure your host meets the minimum system requirements and has activated one of the -PHP opcode caches to achieve the best results for your application. Once your hardware is performing it's best, dig -into the guides below to see what you can do. +PHP opcode caches to achieve the best results for your application. Once your hardware is performing it's best, dig +into the guides below to see what you can do. [CHILDREN Exclude=How_Tos] diff --git a/en/02_Developer_Guides/09_Security/00_Member.md b/en/02_Developer_Guides/09_Security/00_Member.md index a4c0451da..90370f3e4 100644 --- a/en/02_Developer_Guides/09_Security/00_Member.md +++ b/en/02_Developer_Guides/09_Security/00_Member.md @@ -9,19 +9,18 @@ icon: user ## Introduction The [Member](api:SilverStripe\Security\Member) class is used to represent user accounts on a Silverstripe CMS site (including newsletter recipients). - -## Testing For Logged In Users -The [Security](api:SilverStripe\Security\Security) class comes with a static method for getting information about the current logged in user. +## Testing for logged in users -**Security::getCurrentUser()** +The [Security](api:SilverStripe\Security\Security) class comes with a static method for getting information about the current logged in user. -Retrieves the current logged in member. Returns *null* if user is not logged in, otherwise, the Member object is returned. +`Security::getCurrentUser()` retrieves the current logged in member. Returns `null` if user is not logged in, otherwise, the `Member` object is returned. ```php use SilverStripe\Security\Security; -if( $member = Security::getCurrentUser() ) { +$member = Security::getCurrentUser() +if ($member) { // Work with $member } else { // Do non-member stuff @@ -38,23 +37,26 @@ This is the least desirable way of extending the [Member](api:SilverStripe\Secur You can define subclasses of [Member](api:SilverStripe\Security\Member) to add extra fields or functionality to the built-in membership system. ```php +namespace App\Security; + use SilverStripe\Security\Member; -class MyMember extends Member { +class MyMember extends Member +{ private static $db = [ - "Age" => "Int", - "Address" => "Text", + 'Age' => 'Int', + 'Address' => 'Text', ]; } ``` -To ensure that all new members are created using this class, put a call to [api:Injector] in +To ensure that all new members are created using this class, put a call to [`Injector`](api:SilverStripe\Core\Injector\Injector) in `(project)/_config/_config.yml`: ```yml SilverStripe\Core\Injector\Injector: SilverStripe\Security\Member: - class: MyVendor\MyNamespace\MyMemberClass + class: App\Security\MyMemberClass ``` Note that if you want to look this class-name up, you can call `Injector::inst()->get('Member')->ClassName` @@ -66,26 +68,35 @@ details in the newsletter system. This function returns a [FieldList](api:Silve parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there. ```php +namespace App\Security; + use SilverStripe\Forms\TextField; +use SilverStripe\Security\Member; -public function getCMSFields() { - $fields = parent::getCMSFields(); - $fields->insertBefore("HTMLEmail", new TextField("Age")); - $fields->removeByName("JobTitle"); - $fields->removeByName("Organisation"); - return $fields; +class MyMember extends Member +{ + // ... + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + $fields->insertBefore('HTMLEmail', TextField::create('Age')); + $fields->removeByName('JobTitle'); + $fields->removeByName('Organisation'); + return $fields; + } } ``` -## Extending Member or DataObject? +## Extending `Member` or `DataObject` -Basic rule: Class [Member](api:SilverStripe\Security\Member) should just be extended for entities who have some kind of login. -If you have different types of [Member](api:SilverStripe\Security\Member)s in the system, you have to make sure that those with login-capabilities a unique field to be used for the login. -For persons without login-capabilities (e.g. for an address-database), you shouldn't extend [Member](api:SilverStripe\Security\Member) to avoid conflicts -with the Member-database. This enables us to have a different subclass of [Member](api:SilverStripe\Security\Member) for an email-address with login-data, +Basic rule: Class [`Member`](api:SilverStripe\Security\Member) should just be extended for entities who have some kind of login. +If you have different types of [`Member`](api:SilverStripe\Security\Member)s in the system, you have to make sure that those with login-capabilities a unique field to be used for the login. +For persons without login-capabilities (e.g. for an address-database), you shouldn't extend [`Member`](api:SilverStripe\Security\Member) to avoid conflicts +with the `Member` database table. This enables us to have a different subclass of [`Member`](api:SilverStripe\Security\Member) for an email-address with login-data, and another subclass for the same email-address in the address-database. -## Member Role Extension +## `Member` role extension Using inheritance to add extra behaviour or data fields to a member is limiting, because you can only inherit from 1 class. A better way is to use role extensions to add this behaviour. Add the following to your @@ -94,47 +105,46 @@ class. A better way is to use role extensions to add this behaviour. Add the fol ```yml SilverStripe\Security\Member: extensions: - - MyMemberExtension + - App\Extension\MyMemberExtension ``` -A role extension is simply a subclass of [DataExtension](api:SilverStripe\ORM\DataExtension) that is designed to be used to add behaviour to [Member](api:SilverStripe\Security\Member). +A role extension is simply a subclass of [`DataExtension`](api:SilverStripe\ORM\DataExtension) that is designed to be used to add behaviour to [`Member`](api:SilverStripe\Security\Member). The roles affect the entire class - all members will get the additional behaviour. However, if you want to restrict -things, you should add appropriate [Permission::checkMember()](api:SilverStripe\Security\Permission::checkMember()) calls to the role's methods. - +things, you should add appropriate [`Permission::checkMember()`](api:SilverStripe\Security\Permission::checkMember()) calls to the role's methods. ```php -use SilverStripe\Security\Permission; +namespace App\Extension; + +use SilverStripe\Form\FieldList; use SilverStripe\ORM\DataExtension; +use SilverStripe\Security\Permission; -class MyMemberExtension extends DataExtension +class MyMemberExtension extends DataExtension { + // define additional properties + private static $db = [ + 'MyNewField' => 'Text', + ]; + /** * Modify the field set to be displayed in the CMS detail pop-up */ - public function updateCMSFields(FieldList $currentFields) + public function updateCMSFields(FieldList $currentFields) { - // Only show the additional fields on an appropriate kind of use - if(Permission::checkMember($this->owner->ID, "VIEW_FORUM")) { + // Only show the additional fields on an appropriate kind of use + if (Permission::checkMember($this->owner->ID, 'VIEW_FORUM')) { // Edit the FieldList passed, adding or removing fields as necessary } } - // define additional properties - private static $db = []; - private static $has_one = []; - private static $has_many = []; - private static $many_many = []; - private static $belongs_many_many = []; - - public function somethingElse() + public function somethingElse() { // You can add any other methods you like, which you can call directly on the member object. } } - ``` -## Saved User Logins +## Saved user logins Logins can be "remembered" across multiple devices when user checks the "Remember Me" box. By default, a new login token will be created and associated with the device used during authentication. When user logs out, all previously saved tokens @@ -145,7 +155,7 @@ default and this can be modified via [`RememberLoginHash::$token_expiry_days`](a ## Acting as another user Occasionally, it may be necessary not only to check permissions of a particular member, but also to -temporarily assume the identity of another user for certain tasks. E.g. when running a CLI task, +temporarily assume the identity of another user for certain tasks. For example when running a CLI task, it may be necessary to log in as an administrator to perform write operations. You can use `Member::actAs()` method, which takes a member or member id to act as, and a callback @@ -158,13 +168,17 @@ the current user. Note: Take care not to invoke this method to perform any operation the current user should not reasonably be expected to be allowed to do. -E.g. +For example: ```php - use SilverStripe\Control\Director; - use SilverStripe\Security\Security; - use SilverStripe\Security\Member; - use SilverStripe\Dev\BuildTask; +namespace App\Task; + +use App\Model\DataRecord; +use BadMethodCallException; +use SilverStripe\Control\Director; +use SilverStripe\Dev\BuildTask; +use SilverStripe\Security\Member; +use SilverStripe\Security\Security; class CleanRecordsTask extends BuildTask { @@ -174,13 +188,13 @@ class CleanRecordsTask extends BuildTask throw new BadMethodCallException('This task only runs on CLI'); } $admin = Security::findAnAdministrator(); - Member::actAs($admin, function() { + Member::actAs($admin, function () { DataRecord::get()->filter('Dirty', true)->removeAll(); }); } } ``` -## API Documentation +## API documentation [Member](api:SilverStripe\Security\Member) diff --git a/en/02_Developer_Guides/09_Security/01_Access_Control.md b/en/02_Developer_Guides/09_Security/01_Access_Control.md index d9878b8eb..4ef425c98 100644 --- a/en/02_Developer_Guides/09_Security/01_Access_Control.md +++ b/en/02_Developer_Guides/09_Security/01_Access_Control.md @@ -3,40 +3,41 @@ title: Access Control summary: Restrict CMS access to specific groups of users icon: user-lock --- -# Access Control and Page Security +# Access control and page security There is a fairly comprehensive security mechanism in place for Silverstripe CMS. If you want to add premium content to your -site you have to figure this stuff out, and it's not entirely obvious. +site you have to figure this stuff out, and it's not entirely obvious. ## Ways to restrict access There are a number of ways to restrict access in Silverstripe CMS. In the security tab in the CMS you can create groups -that have access to certain parts. The options can be found on the [permissions](/developer_guides/security/permissions) documentation. +that have access to certain parts. The options can be found on the [permissions](/developer_guides/security/permissions) documentation. Once you have groups, you can set access for each page for a particular group. This can be: -* anyone; -* any person who is logged in; -* a specific group. + +- anyone; +- any person who is logged in; +- a specific group. It is unclear how this works for data-objects that are not pages. -## The Security Groups in Silverstripe CMS +## The security groups in Silverstripe CMS In the security tab you can make groups for security. The way this was intended was as follows (this may be a counter intuitive): - * employees - * marketing - * marketing executive +- employees + - marketing + - marketing executive Thus, the further up the hierarchy you go the MORE privileges you can get. Similarly, you could have: - * members - * coordinators - * admins +- members + - coordinators + - admins Where members have some privileges, coordinators slightly more and administrators the most; having each group inheriting -privileges from its parent group. +privileges from its parent group. ## Permission checking is at class level @@ -50,17 +51,16 @@ the admin screens work). Here are my notes trying to figure this stuff out. Not really useful unless you're VERY interested in how exactly SS works. - ### Loading the admin page: looking at security -If you go to [your site]/admin *Director.php* maps the 'admin' URL request through a [Director](api:SilverStripe\Control\Director) rule to the -[CMSMain](api:SilverStripe\CMS\Controllers\CMSMain) controller (see [CMSMain](api:SilverStripe\CMS\Controllers\CMSMain), with no arguments). +If you go to [your site]/admin `Director.php` maps the 'admin' URL request through a [Director](api:SilverStripe\Control\Director) rule to the +[CMSMain](api:SilverStripe\CMS\Controllers\CMSMain) controller (see [CMSMain](api:SilverStripe\CMS\Controllers\CMSMain), with no arguments). *CMSMain.init()* calls its parent which, of all things is called [LeftAndMain](api:SilverStripe\Admin\LeftAndMain). It's in [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) that the -important security checks are made by calling *Permission::check*. +important security checks are made by calling *Permission::check*. -[Security::permissionFailure()](api:SilverStripe\Security\Security::permissionFailure()) is the next utility function you can use to redirect to the login form. +[Security::permissionFailure()](api:SilverStripe\Security\Security::permissionFailure()) is the next utility function you can use to redirect to the login form. -### Customizing Access Checks in CMS Classes +### Customizing access checks in CMS classes see [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) diff --git a/en/02_Developer_Guides/09_Security/02_Permissions.md b/en/02_Developer_Guides/09_Security/02_Permissions.md index ece9c5270..e53e0224f 100644 --- a/en/02_Developer_Guides/09_Security/02_Permissions.md +++ b/en/02_Developer_Guides/09_Security/02_Permissions.md @@ -3,7 +3,7 @@ title: Permissions summary: Customise the permission system in Silverstripe icon: lock --- -# User Permissions +# User permissions ## Introduction @@ -14,13 +14,11 @@ This class implements Silverstripe CMS's permission system. Permissions are defined on a group-by-group basis. To give a permission to a member, go to a group that contains them, and then select the permissions tab, and add that permission to the list. -The simple usage, Permission::check("PERM_CODE") will detect if the currently logged in member has the given permission. - See the API docs for more options. +The simple usage `Permission::check('PERM_CODE')` will detect if the currently logged in member has the given permission. +See the API docs for more options. -**Group ACLs** - -* Call **Permission::check('MY_PERMISSION_CODE')** to see if the current user has MY_PERMISSION_CODE. -* `MY_PERMISSION_CODE` can be loaded into the Security admin on the appropriate group, using the "Permissions" tab. +- Call `Permission::check('MY_PERMISSION_CODE')` to see if the current user has `MY_PERMISSION_CODE`. +- `MY_PERMISSION_CODE` can be loaded into the Security admin on the appropriate group, using the "Permissions" tab. ## PermissionProvider @@ -28,31 +26,31 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo This method should return a map of permission code names with a human readable explanation of its purpose. ```php -use SilverStripe\Security\Permission; -use SilverStripe\Security\PermissionProvider; -use SilverStripe\Security\Security; +namespace { + use SilverStripe\Security\Permission; + use SilverStripe\Security\PermissionProvider; + use SilverStripe\Security\Security; -class PageController implements PermissionProvider -{ - public function init() + class PageController implements PermissionProvider { - parent::init(); - if (!Permission::check('VIEW_SITE')) { - Security::permissionFailure(); + public function init() + { + parent::init(); + if (!Permission::check('VIEW_SITE')) { + Security::permissionFailure(); + } } - } - public function providePermissions() - { - return [ - 'VIEW_SITE' => 'Access the site' - ]; + public function providePermissions() + { + return [ + 'VIEW_SITE' => 'Access the site', + ]; + } } } - ``` - This can then be used to add a dropdown for permission codes to the security panel. `Permission::get_all_codes()` will be a helper method that will call `providePermissions()` on every applicable class, and collate the results into a single dropdown. @@ -61,23 +59,22 @@ dropdown. By default, permissions are used in the following way: -* The 'View' permission is checked when opening a page -* The 'View' permissions is used on **all** default datafeeds: - * If not logged in, the 'View' permissions must be 'anyone logged in' for a page to be displayed in a menu - * If logged in, you must be allowed to view a page for it to be displayed in a menu - +- The 'View' permission is checked when opening a page +- The 'View' permissions is used on **all** default datafeeds: + - If not logged in, the 'View' permissions must be 'anyone logged in' for a page to be displayed in a menu + - If logged in, you must be allowed to view a page for it to be displayed in a menu ## Setting up permissions -* By default, permissions are linked to groups. You define a many-many relationship called Can(permname), eg, +- By default, permissions are linked to groups. You define a many-many relationship called Can(permname), eg, "CanView". Please note that group permissions are more efficient, as SQL joins are used to filter data. -* Alternatively, you can create a custom permission by defining a function called can(permname) +- Alternatively, you can create a custom permission by defining a function called can(permname) ## Using permissions -* On an individual data record, $page->can("View", $member = null) and be called. If a member isn't passed, the +- On an individual data record, $page->can("View", $member = null) and be called. If a member isn't passed, the currently logged in member is assumed. -* On a request, $request->hasPermission("View", $member = null) can be called. See [datamodel](/developer_guides/model/permissions) for +- On a request, $request->hasPermission("View", $member = null) can be called. See [datamodel](/developer_guides/model/permissions) for information on request objects. ## Special cases @@ -98,19 +95,20 @@ The `CMS_ACCESS_LeftAndMain` grants access to every single area of the CMS, with adding the `CMS_ACCESS_LeftAndMain` code to the set of accepted codes when a `CMS_ACCESS_*` permission is required. This works much like ADMIN permissions (see above) - #### 2. Checking for any access to the CMS You can check if a user has access to the CMS by simply performing a check against `CMS_ACCESS`. ```php -if (SilverStripe\Security\Permission::checkMember($member, 'CMS_ACCESS')) { +use SilverStripe\Security\Permission; + +if (Permission::checkMember($member, 'CMS_ACCESS')) { //user can access the CMS } ``` Internally, this checks that the user has any of the defined `CMS_ACCESS_*` permissions. +## API documentation -## API Documentation [Permission](api:SilverStripe\Security\Permission) diff --git a/en/02_Developer_Guides/09_Security/03_Authentication.md b/en/02_Developer_Guides/09_Security/03_Authentication.md index 05a1a20c8..6d3b87838 100644 --- a/en/02_Developer_Guides/09_Security/03_Authentication.md +++ b/en/02_Developer_Guides/09_Security/03_Authentication.md @@ -1,6 +1,6 @@ --- title: Authentication -summary: Explains Silverstripe CMS's Authentication options and custom authenticators. +summary: Explains Silverstripe CMS's Authentication options and custom authenticators. icon: users-cog --- @@ -9,12 +9,12 @@ icon: users-cog By default, Silverstripe CMS provides a [MemberAuthenticator](api:SilverStripe\Security\MemberAuthenticator\MemberAuthenticator) class which hooks into its own internal authentication system. -## User Interface +## User interface Silverstripe CMS comes with a default login form interface, that's embedded into your page templates through the `$Form` placeholder. Since it's embedded into your own site styling and behaviour, -it can require adjustments to your particular context. +it can require adjustments to your particular context. The view logic may be handled through the [silverstripe/login-forms](https://github.com/silverstripe/silverstripe-login-forms) module (if present). @@ -27,26 +27,26 @@ The main login system uses these controllers to handle the various security requ [CMSSecurity](api:SilverStripe\Security\CMSSecurity) - Which is the controller which handles security requests within the CMS, and allows users to re-login without leaving the CMS. -## Member Authentication +## Member authentication The default member authentication system is implemented in the following classes: [MemberAuthenticator](api:SilverStripe\Security\MemberAuthenticator) - Which is the default member authentication implementation. This uses the email and password stored internally for each member to authenticate them. -[MemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\MemberLoginForm) - Is the default form used by `MemberAuthenticator`, and is displayed on the public site at the url `Security/login` by default. +[MemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\MemberLoginForm) - Is the default form used by `MemberAuthenticator`, and is displayed on the public site at the URL `Security/login` by default. -[CMSMemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm) - Is the secondary form used by `MemberAuthenticator`, and will be displayed to the user within the CMS any time their session expires or they are logged out via an action. This form is presented via a popup dialog, and can be used to re-authenticate that user automatically without them having to lose their workspace. E.g. if editing a form, the user can login and continue to publish their content. +[CMSMemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm) - Is the secondary form used by `MemberAuthenticator`, and will be displayed to the user within the CMS any time their session expires or they are logged out via an action. This form is presented via a popup dialog, and can be used to re-authenticate that user automatically without them having to lose their workspace. For example if editing a form, the user can login and continue to publish their content. -## Custom Authentication +## Custom authentication Additional authentication methods (oauth, etc) can be implemented by creating custom implementations of each of the following base classes: -[Authenticator](api:SilverStripe\Security\Authenticator) - The base class for authentication systems. This class also acts as the factory to generate various login forms for parts of the system. If an authenticator supports in-cms reauthentication then it will be necessary to override the `supports_cms` and `get_cms_login_form` methods. +[Authenticator](api:SilverStripe\Security\Authenticator) - The base class for authentication systems. This class also acts as the factory to generate various login forms for parts of the system. If an authenticator supports in-cms reauthentication then it will be necessary to override the `supports_cms` and `get_cms_login_form` methods. [LoginForm](api:SilverStripe\Security\LoginForm) - which is the base class for a login form which links to a specific authenticator. At the very least, it will be necessary to implement a form class which provides a default login interface. If in-cms re-authentication is desired, then a specialised subclass of this method may be necessary. For example, this form could be extended to require confirmation of username as well as password. -## Default Admin +## Default admin When a new Silverstripe CMS site is created for the first time, it may be necessary to create a default admin to provide CMS access for the first time. Silverstripe CMS provides a default admin configuration system, which allows a username @@ -54,7 +54,7 @@ and password to be configured for a single special user outside of the normal me It is advisable to configure this user in your `.env` file inside of the project root, as below: -``` +```bash # Configure a default username and password to access the CMS on all sites in this environment. SS_DEFAULT_ADMIN_USERNAME="admin" SS_DEFAULT_ADMIN_PASSWORD="password" @@ -64,17 +64,19 @@ When a user logs in with these credentials, then a [Member](api:SilverStripe\Sec the database, but without any password information. This means that the password can be reset or changed by simply updating the `.env` file. -## Registering a new Authenticator +## Registering a new authenticator -```yaml +```yml SilverStripe\Core\Injector\Injector: SilverStripe\Security\Security: properties: Authenticators: - myauthenticator: '%$MyVendor\MyProject\Authenticator\MyAuthenticator' + myauthenticator: '%$App\Authenticator\MyAuthenticator' ``` + If there is no authenticator registered, `Authenticator` will try to fall back on the default provided authenticator (`default`), which can be changed using the following config, replacing the MemberAuthenticator with your authenticator: -```yaml + +```yml --- Name: MyAuth After: @@ -84,7 +86,7 @@ SilverStripe\Core\Injector\Injector: SilverStripe\Security\Security: properties: Authenticators: - default: '%$MyVendor\MyProject\Authenticator\MyAuthenticator' + default: '%$App\Authenticator\MyAuthenticator' ``` By default, the `SilverStripe\Security\MemberAuthenticator\MemberAuthenticator` is seen as the default authenticator until it's explicitly set in the config. @@ -113,14 +115,16 @@ public function supportedServices(); If there is no available authenticator for the required action (either one of the constants above), an error will be thrown. Custom Authenticators are expected to have the following methods implemented: -* `getLoginHandler()` -* `getLogoutHandler()` -* `getChangePasswordHandler()` -* `getLostPasswordHandler()` + +- `getLoginHandler()` +- `getLogoutHandler()` +- `getChangePasswordHandler()` +- `getLostPasswordHandler()` All expect a `$link` variable, to handle the request. -Further, there is -* `authenticate()` +Further, there is + +- `authenticate()` Which expects the data to be used for authentication as an array and a nullable variable `$result` by reference, which returns a `ValidationResult`. If only a subset of the supportedServices() will be provided by the custom Authenticator, it is advised to extend `SilverStripe\Security\MemberAuthenticator\MemberAuthenticator`, as that default contains all required methods already and only an override or follow up needs to be written. @@ -130,9 +134,10 @@ An example of how to write a multi-factor authentication [can be found here](htt ## IdentityStore A new IdentityStore, e.g. an LDAP IdentityStore can be registered as follows in a `security.yml` file (Not an actual valid LDAP configuration): -```yaml + +```yml SilverStripe\Core\Injector\Injector: - MyProject\LDAP\Authenticator\LDAPAuthenticator: + App\LDAP\Authenticator\LDAPAuthenticator: properties: LDAPSettings: - URL: https://my-ldap-location.com @@ -141,13 +146,13 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\Security\RequestAuthenticationHandler properties: Handlers: - ldap: '%$MyProject\LDAP\Authenticator\LDAPAuthenticator' + ldap: '%$App\LDAP\Authenticator\LDAPAuthenticator' ``` CascadeInTo is used to defer login or logout actions to other authenticators, after the first one has been logged in. In the example of LDAP authenticator, this is useful to check e.g. the validity of the Session (is the user still logged in?) and if not, or it's LDAP login period has expired, only then validate against the external service again, limiting the amount of requests to the external service. -Upon request, the Member is authenticated against the given AuthenticatorHandlers. To override an Authenticator, override it's name in the `YML` to your own Handler. +Upon request, the Member is authenticated against the given AuthenticatorHandlers. To override an Authenticator, override it's name in the `YAML` to your own Handler. -To get applicable Authenticators for a certain request, refer to [API:Security:getApplicableAuthenticators()]. +To get applicable Authenticators for a certain request, refer to [`Security::getApplicableAuthenticators()`](api:SilverStripe\Security\Security::getApplicableAuthenticators()). -To register `CMS` authenticators, use the same procedure as above, only replace `SilverStripe\Security\Security` with `SilverStripe\Security\CMSSecurity`. +To register `CMS` authenticators, use the same procedure as above, only replace `SilverStripe\Security\Security` with `SilverStripe\Security\CMSSecurity`. diff --git a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md index 350db0844..5dccf1453 100644 --- a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md +++ b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md @@ -4,7 +4,7 @@ summary: Require users to verify their identity when performing sensitive action icon: key --- -# Sudo Mode +# Sudo mode Sudo mode represents a heightened level of permission in that you are more certain that the current user is actually the person whose account is logged in. This is performed by re-validating that the account's password is correct, and will then last for a certain amount of time (configurable) until it will be checked again. @@ -14,7 +14,7 @@ Sudo mode will automatically be enabled for the configured lifetime when a user The default [`SudoModeServiceInterface`](api:SilverStripe\Security\SudoMode\SudoModeServiceInterface) implementation is [`SudoModeService`](api:SilverStripe\Security\SudoMode\SudoModeService), and its lifetime can be configured with YAML. You should read the lifetime value using `SudoModeServiceInterface::getLifetime()`. -```yaml +```yml SilverStripe\Security\SudoMode\SudoModeService: lifetime_minutes: 25 ``` @@ -24,6 +24,8 @@ SilverStripe\Security\SudoMode\SudoModeService: You can add the `SudoModeServiceInterface` singleton as a dependency to a controller that requires sudo mode for one of its actions: ```php +namespace App\Control; + class MyController extends Controller { private ?SudoModeServiceInterface $sudoModeService = null; @@ -41,16 +43,23 @@ class MyController extends Controller Performing a sudo mode verification check in a controller action is simply using the service to validate the request: ```php -public function myAction(HTTPRequest $request): HTTPResponse +namespace App\Control; + +class MyController extends Controller { - if (!$this->sudoModeService->check($request->getSession())) { - return $this->httpError(403, 'Sudo mode is required for this action'); + // ... + + public function myAction(HTTPRequest $request): HTTPResponse + { + if (!$this->sudoModeService->check($request->getSession())) { + return $this->httpError(403, 'Sudo mode is required for this action'); + } + // ... continue with sensitive operations } - // ... continue with sensitive operations } ``` -## Using sudo mode in a React component +## Using sudo mode in a react component The `silverstripe/admin` module defines a [React Higher-Order-Component](https://reactjs.org/docs/higher-order-components.html) (aka HOC) which can be applied to React components in your module or code to intercept component rendering and show a "sudo mode required" @@ -63,7 +72,7 @@ The `WithSudoMode` HOC is exposed via [Webpack's expose-loader plugin](https://w You can get the injector to apply the HOC to your component automatically using [injector transformations](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#transforming-services-using-middleware): -```jsx +```js import WithSudoMode from 'containers/SudoMode/SudoMode'; Injector.transform('MyComponentWithSudoMode', (updater) => { diff --git a/en/02_Developer_Guides/09_Security/05_Secure_Coding.md b/en/02_Developer_Guides/09_Security/05_Secure_Coding.md index 6e1bd6b05..9da685489 100644 --- a/en/02_Developer_Guides/09_Security/05_Secure_Coding.md +++ b/en/02_Developer_Guides/09_Security/05_Secure_Coding.md @@ -8,10 +8,10 @@ icon: user-secret ## Introduction -This page details notes on how to ensure that we develop secure Silverstripe CMS applications. +This page details notes on how to ensure that we develop secure Silverstripe CMS applications. See [Reporting Security Issues](/contributing/issues_and_bugs#reporting-security-issues) on how to report potential vulnerabilities. -## SQL Injection +## SQL injection The [coding-conventions](/contributing/coding_conventions) help guard against SQL injection attacks but still require developer diligence: ensure that any variable you insert into a filter / sort / join clause is either parameterised, or has been @@ -27,7 +27,7 @@ the parameters passed in to be executed. Many DB adaptors support these as stand [SQLite](https://php.net/manual/en/sqlite3.prepare.php), and [PostgreSQL](https://php.net/manual/en/function.pg-prepare.php). The use of parameterised queries whenever possible will safeguard your code in most cases, but care -must still be taken when working with literal values or table/column identifiers that may +must still be taken when working with literal values or table/column identifiers that may come from user input. Example: @@ -48,18 +48,17 @@ $records = SQLSelect::create()->addWhere(['"ID"' => 3])->execute(); Parameterised updates and inserts are also supported, but the syntax is a little different - ```php -use SilverStripe\ORM\Queries\SQLInsert; use SilverStripe\ORM\DB; +use SilverStripe\ORM\Queries\SQLInsert; SQLInsert::create('"MyClass"') ->assign('"Name"', 'Daniel') ->addAssignments([ '"Position"' => 'Accountant', '"Age"' => [ - 'GREATEST(0,?,?)' => [24, 28] - ] + 'GREATEST(0,?,?)' => [24, 28], + ], ]) ->assignSQL('"Created"', 'NOW()') ->execute(); @@ -76,16 +75,16 @@ Silverstripe CMS internally will use parameterised queries in SQL statements whe If necessary Silverstripe performs any required escaping through database-specific methods (see [`Database::addslashes()`](api:SilverStripe\ORM\Connect\Database::addslashes())). For [`MySQLDatabase`](api:SilverStripe\ORM\Connect\MySQLDatabase), this will be [`mysqli::real_escape_string()`](https://www.php.net/manual/en/mysqli.real-escape-string.php). -* Most [`DataList`](api:SilverStripe\ORM\DataList) accessors (see escaping note in method documentation) -* [`DataObject::get_by_id()`](api:SilverStripe\ORM\DataObject::get_by_id()) -* [`DataObject::update()`](api:SilverStripe\ORM\DataObject::update()) -* [`DataObject::castedUpdate()`](api:SilverStripe\ORM\DataObject::castedUpdate()) -* `$dataObject->SomeField = 'val'`, [`DataObject::setField()`](api:SilverStripe\ORM\DataObject::setField()) -* [`DataObject::write()`](api:SilverStripe\ORM\DataObject::write()) -* [`DataList::byID()`](api:SilverStripe\ORM\DataList::byID()) -* [`Form::saveInto()`](api:SilverStripe\Forms\Form::saveInto()) -* [`FormField::saveInto()`](api:SilverStripe\Forms\FormField::saveInto()) -* [`DBField::saveInto()`](api:SilverStripe\ORM\FieldType\DBField::saveInto()) +- Most [`DataList`](api:SilverStripe\ORM\DataList) accessors (see escaping note in method documentation) +- [`DataObject::get_by_id()`](api:SilverStripe\ORM\DataObject::get_by_id()) +- [`DataObject::update()`](api:SilverStripe\ORM\DataObject::update()) +- [`DataObject::castedUpdate()`](api:SilverStripe\ORM\DataObject::castedUpdate()) +- `$dataObject->SomeField = 'val'`, [`DataObject::setField()`](api:SilverStripe\ORM\DataObject::setField()) +- [`DataObject::write()`](api:SilverStripe\ORM\DataObject::write()) +- [`DataList::byID()`](api:SilverStripe\ORM\DataList::byID()) +- [`Form::saveInto()`](api:SilverStripe\Forms\Form::saveInto()) +- [`FormField::saveInto()`](api:SilverStripe\Forms\FormField::saveInto()) +- [`DBField::saveInto()`](api:SilverStripe\ORM\FieldType\DBField::saveInto()) Data is not escaped when writing to object-properties, as inserts and updates are normally handled via prepared statements. @@ -96,13 +95,13 @@ Example: use SilverStripe\Security\Member; // automatically escaped/quoted -$members = Member::get()->filter('Name', $_GET['name']); +$members = Member::get()->filter('Name', $_GET['name']); // automatically escaped/quoted -$members = Member::get()->filter(['Name' => $_GET['name']]); +$members = Member::get()->filter(['Name' => $_GET['name']]); // parameterised condition -$members = Member::get()->where(['"Name" = ?' => $_GET['name']]); +$members = Member::get()->where(['"Name" = ?' => $_GET['name']]); // needs to be escaped and quoted manually (note raw2sql called with the $quote parameter set to true) -$members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true))); +$members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true))); ``` [warning] @@ -116,49 +115,58 @@ As a rule of thumb, whenever you're creating SQL queries (or just chunks of SQL) but there may be cases where you need to take care of escaping yourself. See [coding-conventions](/getting_started/coding-conventions) and [datamodel](/developer_guides/model) for ways to parameterise, cast, and convert your data. -* [`SQLSelect`](api:SilverStripe\ORM\Queries\SQLSelect) -* [`DB::query()`](api:SilverStripe\ORM\DB::query()) -* [`DB::prepared_query()`](api:SilverStripe\ORM\DB::prepared_query()) -* `Controller->requestParams` -* `Controller->urlParams` -* [`HTTPRequest`](api:SilverStripe\Control\HTTPRequest) data -* GET/POST data passed to a form method +- [`SQLSelect`](api:SilverStripe\ORM\Queries\SQLSelect) +- [`DB::query()`](api:SilverStripe\ORM\DB::query()) +- [`DB::prepared_query()`](api:SilverStripe\ORM\DB::prepared_query()) +- `Controller->requestParams` +- `Controller->urlParams` +- [`HTTPRequest`](api:SilverStripe\Control\HTTPRequest) data +- `GET`/`POST` data passed to a form method Example: ```php +namespace App\Form; + +use App\Model\Player; use SilverStripe\Core\Convert; use SilverStripe\Forms\Form; -class MyForm extends Form +class MyForm extends Form { - public function save($RAW_data, $form) + public function save($RAW_data, $form) { // Pass true as the second parameter of raw2sql to quote the value safely - $SQL_data = Convert::raw2sql($RAW_data, true); // works recursively on an array - $objs = Player::get()->where("Name = " . $SQL_data['name']); + // works recursively on an array + $SQL_data = Convert::raw2sql($RAW_data, true); + $objs = Player::get()->where('Name = ' . $SQL_data['name']); // ... } } ``` -* `FormField->Value()` -* URLParams passed to a Controller-method +- `FormField->Value()` +- URLParams passed to a Controller-method Example: ```php -use SilverStripe\Core\Convert; +namespace App\Control; + +use App\Model\Player; use SilverStripe\Control\Controller; +use SilverStripe\Core\Convert; -class MyController extends Controller +class MyController extends Controller { private static $allowed_actions = ['myurlaction']; - public function myurlaction($RAW_urlParams) + + public function myurlaction($RAW_urlParams) { // Pass true as the second parameter of raw2sql to quote the value safely - $SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); // works recursively on an array - $objs = Player::get()->where("Name = " . $SQL_data['OtherID']); + // works recursively on an array + $SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); + $objs = Player::get()->where('Name = ' . $SQL_data['OtherID']); // ... } } @@ -168,24 +176,27 @@ As a rule of thumb, you should escape your data **as close to querying as possib (or preferably, use parameterised queries). This means if you've got a chain of functions passing data through, escaping should happen at the end of the chain. - ```php +namespace App\Control; + +use SilverStripe\Control\Controller; use SilverStripe\Core\Convert; use SilverStripe\ORM\DB; -use SilverStripe\Control\Controller; -class MyController extends Controller +class MyController extends Controller { /** * @param array $RAW_data All names in an indexed array (not SQL-safe) */ - public function saveAllNames($RAW_data) + public function saveAllNames($RAW_data) { // $SQL_data = Convert::raw2sql($RAW_data); // premature escaping - foreach($RAW_data as $item) $this->saveName($item); + foreach ($RAW_data as $item) { + $this->saveName($item); + } } - public function saveName($RAW_name) + public function saveName($RAW_name) { $SQL_name = Convert::raw2sql($RAW_name, true); DB::query("UPDATE Player SET Name = {$SQL_name}"); @@ -197,7 +208,7 @@ This might not be applicable in all cases - especially if you are building an AP you're passing unescaped data, make sure to be explicit about it by writing *phpdoc*-documentation and *prefixing* your variables ($RAW_data instead of $data). -## XSS (Cross-Site-Scripting) +## XSS (cross-site-scripting) Silverstripe CMS helps you guard any output against clientside attacks initiated by malicious user input, commonly known as XSS (Cross-Site-Scripting). With some basic guidelines, you can ensure your output is safe for a specific use case (e.g. @@ -238,10 +249,10 @@ SilverStripe\Forms\HTMLEditor\HTMLEditorField: sanitise_server_side: false ``` -Note it is not currently possible to allow editors to provide javascript content and yet still protect other users -from any malicious code within that javascript. +Note it is not currently possible to allow editors to provide JavaScript content and yet still protect other users +from any malicious code within that JavaScript. -We recommend configuring [shortcodes](/developer_guides/extending/shortcodes) that can be used by editors in place of using javascript directly. +We recommend configuring [shortcodes](/developer_guides/extending/shortcodes) that can be used by editors in place of using JavaScript directly. ### Escaping model properties @@ -250,27 +261,28 @@ object-properties by [casting](/developer_guides/model/data_types_and_casting) i PHP: - ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyObject extends DataObject +class MyObject extends DataObject { private static $db = [ - 'MyEscapedValue' => 'Text', // Example value: not bold - 'MyUnescapedValue' => 'HTMLText' // Example value: bold + // Example value: not bold + 'MyEscapedValue' => 'Text', + // Example value: bold + 'MyUnescapedValue' => 'HTMLText', ]; } - ``` Template: - -```php -
      -
    • $MyEscapedValue
    • // output: <b>not bold<b> -
    • $MyUnescapedValue
    • // output: bold +```ss +
        +
      • $MyEscapedValue
      • <%-- output: <b>not bold<b> --%> +
      • $MyUnescapedValue
      • <%-- output: bold --%>
      ``` @@ -280,18 +292,17 @@ outputting through SSViewer. ### Overriding default escaping in templates You can force escaping on a casted value/object by using an [escape type](/developer_guides/model/data_types_and_casting) method in your template, e.g. -"XML" or "ATT". +"XML" or "ATT". Template (see above): - -```php -
        - // output: foo & "bar" -
      • $Title
      • -
      • $MyEscapedValue
      • // output: <b>not bold<b> -
      • $MyUnescapedValue
      • // output: bold -
      • $MyUnescapedValue.XML
      • // output: <b>bold<b> +```ss +
          + <%-- output: foo & "bar" --%> +
        • $Title
        • +
        • $MyEscapedValue
        • <%-- output: <b>not bold<b> --%> +
        • $MyUnescapedValue
        • <%-- output: bold --%> +
        • $MyUnescapedValue.XML
        • <%-- output: <b>bold<b> --%>
        ``` @@ -303,33 +314,47 @@ static *$casting* array. Caution: Casting only applies when using values in a te PHP: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyObject extends DataObject +class MyObject extends DataObject { - public $Title = 'not bold'; // will be escaped due to Text casting - - $casting = [ - "Title" => "Text", // forcing a casting - 'TitleWithHTMLSuffix' => 'HTMLText' // optional, as HTMLText is the default casting + private string $title = 'not bold'; + + private static $casting = [ + // forcing a casting + 'Title' => 'Text', + // optional, as HTMLText is the default casting + 'TitleWithHTMLSuffix' => 'HTMLText', ]; - - public function TitleWithHTMLSuffix($suffix) + + public function getTitle() { - // $this->Title is not casted in PHP - return $this->Title . '(' . $suffix. ')'; + // will be escaped due to Text casting + return $this->title; + } + + public function getTitleWithHTMLSuffix($suffix) + { + // $this->getTitle() is not casted in PHP + return $this->getTitle() . '(' . $suffix . ')'; } } ``` -Template: +[info] +If `$title` was a public property called `$Title`, it would also be casted the same way that the result of +`$getTitle()` is casted. +[/info] +Template: -```php -
          -
        • $Title
        • // output: <b>not bold<b> -
        • $Title.RAW
        • // output: not bold -
        • $TitleWithHTMLSuffix
        • // output: not bold: (...) +```ss +
            +
          • $Title
          • <%-- output: <b>not bold<b> --%> +
          • $Title.RAW
          • <%-- output: not bold --%> +
          • $TitleWithHTMLSuffix
          • <%-- output: not bold: (...) --%>
          ``` @@ -339,7 +364,7 @@ presentation from business logic. ### Manual escaping in PHP When using *customise()* or *renderWith()* calls in your controller, or otherwise forcing a custom context for your -template, you'll need to take care of casting and escaping yourself in PHP. +template, you'll need to take care of casting and escaping yourself in PHP. The [Convert](api:SilverStripe\Core\Convert) class has utilities for this, mainly *Convert::raw2xml()* and *Convert::raw2att()* (which is also used by *XML* and *ATT* in template code). @@ -348,12 +373,16 @@ also used by *XML* and *ATT* in template code). Most of the `Convert::raw2` methods accept arrays and do not affect array keys. If you serialize your data, make sure to do that before you pass it to `Convert::raw2` methods. -E.g.: +For example: ```php -json_encode(Convert::raw2sql($request->getVar('multiselect'))); // WRONG! +use SilverStripe\Core\Convert; + +// WRONG! +json_encode(Convert::raw2sql($request->getVar('multiselect'))); -Convert::raw2sql(json_encode($request->getVar('multiselect'))); // Correct! +// Correct! +Convert::raw2sql(json_encode($request->getVar('multiselect'))); ``` [/warning] @@ -361,20 +390,23 @@ Convert::raw2sql(json_encode($request->getVar('multiselect'))); // Correct! PHP: ```php -use SilverStripe\Core\Convert; +namespace App\Control; + use SilverStripe\Control\Controller; -use SilverStripe\ORM\FieldType\DBText; +use SilverStripe\Core\Convert; use SilverStripe\ORM\FieldType\DBHTMLText; +use SilverStripe\ORM\FieldType\DBText; -class MyController extends Controller +class MyController extends Controller { private static $allowed_actions = ['search']; - public function search($request) + + public function search($request) { $htmlTitle = '

          Your results for:' . Convert::raw2xml($request->getVar('Query')) . '

          '; return $this->customise([ 'Query' => DBText::create($request->getVar('Query')), - 'HTMLTitle' => DBHTMLText::create($htmlTitle) + 'HTMLTitle' => DBHTMLText::create($htmlTitle), ]); } } @@ -382,8 +414,8 @@ class MyController extends Controller Template: -```php -

          $HTMLTitle

          +```ss +

          $HTMLTitle

          ``` Whenever you insert a variable into an HTML attribute within a template, use $VarName.ATT, no not $VarName. @@ -397,20 +429,22 @@ user data, not *Convert::raw2att()*. Use raw ampersands in your URL, and cast t PHP: - ```php +namespace App\Control; + use SilverStripe\Control\Controller; use SilverStripe\ORM\FieldType\DBText; -class MyController extends Controller +class MyController extends Controller { private static $allowed_actions = ['search']; - public function search($request) + + public function search($request) { - $rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc"; + $rssRelativeLink = '/rss?Query=' . urlencode($_REQUEST['query']) . '&sortOrder=asc'; $rssLink = Controller::join_links($this->Link(), $rssRelativeLink); return $this->customise([ - "RSSLink" => DBText::create($rssLink), + 'RSSLink' => DBText::create($rssLink), ]); } } @@ -418,17 +452,16 @@ class MyController extends Controller Template: - -```php +```ss RSS feed ``` Some rules of thumb: -* Don't concatenate URLs in a template. It only works in extremely simple cases that usually contain bugs. -* Use *Controller::join_links()* to concatenate URLs. It deals with query strings and other such edge cases. +- Don't concatenate URLs in a template. It only works in extremely simple cases that usually contain bugs. +- Use *Controller::join_links()* to concatenate URLs. It deals with query strings and other such edge cases. -## Cross-Site Request Forgery (CSRF) +## Cross-Site request forgery (CSRF) Silverstripe CMS has built-in countermeasures against CSRF identity theft for all form submissions. A form object will automatically contain a `SecurityID` parameter which is generated as a secure hash on the server, connected to the @@ -437,7 +470,7 @@ match the hash stored in the users session, the request is discarded. You can disable this behaviour through [Form::disableSecurityToken()](api:SilverStripe\Forms\Form::disableSecurityToken()). It is also recommended to limit form submissions to the intended HTTP verb (mostly `GET` or `POST`) -through [Form::setStrictFormMethodCheck()](api:SilverStripe\Forms\Form::setStrictFormMethodCheck()). +through [Form::setStrictFormMethodCheck()](api:SilverStripe\Forms\Form::setStrictFormMethodCheck()). Sometimes you need to handle state-changing HTTP submissions which aren't handled through Silverstripe CMS's form system. In this case, you can also check the current HTTP request @@ -459,39 +492,45 @@ passed, such as *example.com/home/add/dfsdfdsfd*, then it returns 0. Below is an example with different ways you would use this casting technique: - ```php -public function CaseStudies() +namespace App\PageType; + +use App\Model\CaseStudy; +use Page; +use SilverStripe\Control\Director; + +class CaseStudyPage extends Page { + public function getCaseStudies() + { + // cast an ID from URL parameters e.g. (example.com/home/action/ID) + $anotherID = (int) Director::urlParam['ID']; + + // perform a calculation, the prerequisite being $anotherID must be an integer + $calc = $anotherID + (5 - 2) / 2; - // cast an ID from URL parameters e.g. (example.com/home/action/ID) - $anotherID = (int)Director::urlParam['ID']; - - // perform a calculation, the prerequisite being $anotherID must be an integer - $calc = $anotherID + (5 - 2) / 2; - - // cast the 'category' GET variable as an integer - $categoryID = (int)$_GET['category']; - - // perform a byID(), which ensures the ID is an integer before querying - return CaseStudy::get()->byID($categoryID); + // cast the 'category' GET variable as an integer + $categoryID = (int) $_GET['category']; + + // perform a byID(), which ensures the ID is an integer before querying + return CaseStudy::get()->byID($categoryID); + } } ``` The same technique can be employed anywhere in your PHP code you know something must be of a certain type. A list of PHP cast types can be found here: -* `(int)`, `(integer)` - cast to integer -* `(bool)`, `(boolean)` - cast to boolean -* `(float)`, `(double)`, `(real)` - cast to float -* `(string)` - cast to string -* `(array)` - cast to array -* `(object)` - cast to object +- `(int)`, `(integer)` - cast to integer +- `(bool)`, `(boolean)` - cast to boolean +- `(float)`, `(double)`, `(real)` - cast to float +- `(string)` - cast to string +- `(array)` - cast to array +- `(object)` - cast to object Note that there is also a 'Silverstripe CMS' way of casting fields on a class, this is a different type of casting to the standard PHP way. See [casting](/developer_guides/model/data_types_and_casting). - ## Filesystem ### Don't allow script-execution in /assets @@ -504,7 +543,7 @@ for instructions on how to secure the assets folder against malicious script exe Silverstripe routes all execution through a [`public/` subfolder](/getting_started/directory_structure) by default. This enables you to keep application code and configuration outside of webserver routing. -``` +```text .htaccess <- fallback, shouldn't be used public/ <- this should be your webroot .htaccess @@ -545,14 +584,14 @@ salt values generated with the strongest entropy generators available on the pla [Rainbow tables](https://en.wikipedia.org/wiki/Rainbow_table). Strong passwords are a crucial part of any system security. So in addition to storing the password in a secure fashion, -you can also enforce specific password policies by configuring a +you can also enforce specific password policies by configuring a [PasswordValidator](api:SilverStripe\Security\PasswordValidator). This can be done through a `_config.php` file at runtime, or via YAML configuration. The default password validation rules are configured in the framework's `passwords.yml` file. You will need to ensure that your config file is processed after it. -```yaml +```yml --- Name: mypasswords After: '#corepasswords' @@ -576,7 +615,7 @@ SilverStripe\Security\PasswordValidator: The default password validation character strength tests can be seen in the `PasswordValidator.character_strength_tests` configuration property. You can add your own with YAML config, by providing a name for it and a regex pattern to match: -```yaml +```yml SilverStripe\Security\PasswordValidator: character_strength_tests: contains_secret_word: '/1337pw/' @@ -588,31 +627,32 @@ This will ensure that a password contains `1337pw` somewhere in the string befor In addition, you can tighten password security with the following configuration settings: - * `Member.password_expiry_days`: Set the number of days that a password should be valid for. - * `Member.lock_out_after_incorrect_logins`: Number of incorrect logins after which +- `Member.password_expiry_days`: Set the number of days that a password should be valid for. +- `Member.lock_out_after_incorrect_logins`: Number of incorrect logins after which the user is blocked from further attempts for the timespan defined in `$lock_out_delay_mins` - * `Member.lock_out_delay_mins`: Minutes of enforced lockout after incorrect password attempts. Only applies if `lock_out_after_incorrect_logins` is greater than 0. - * `Security.remember_username`: Set to false to disable autocomplete on login form - * `Session.timeout`: Set timeout to attenuate the risk of active sessions being exploited +- `Member.lock_out_delay_mins`: Minutes of enforced lockout after incorrect password attempts. Only applies if `lock_out_after_incorrect_logins` is greater than 0. +- `Security.remember_username`: Set to false to disable autocomplete on login form +- `Session.timeout`: Set timeout to attenuate the risk of active sessions being exploited -## Clickjacking: Prevent iframe Inclusion +## Clickjacking: prevent iframe inclusion "[Clickjacking](https://owasp.org/www-community/attacks/Clickjacking)" is a malicious technique where a web user is tricked into clicking on hidden interface elements, which can lead to the attacker gaining access to user data or taking control of the website behaviour. -You can signal to browsers that the current response isn't allowed to be +You can signal to browsers that the current response isn't allowed to be included in HTML "frame" or "iframe" elements, and thereby prevent the most common attack vector. This is done through a HTTP header, which is usually added in your controller's `init()` method: - ```php +namespace App\Control; + use SilverStripe\Control\Controller; -class MyController extends Controller +class MyController extends Controller { - public function init() + public function init() { parent::init(); $this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN'); @@ -629,13 +669,13 @@ as well as the login form. To prevent a forged hostname appearing being used by the application, Silverstripe CMS allows the configure of a whitelist of hosts that are allowed to access the system. By defining this whitelist in your `.env` file, any request presenting a `Host` header that is -_not_ in this list will be blocked with a HTTP 400 error: +*not* in this list will be blocked with a HTTP 400 error: -``` +```bash SS_ALLOWED_HOSTS="www.example.com,example.com,subdomain.example.com" ``` -Please note that if this configuration is defined, you _must_ include _all_ subdomains (eg www.) +Please note that if this configuration is defined, you *must* include *all* subdomains (eg `.`) that will be accessing the site. When Silverstripe CMS is run behind a reverse proxy, it's normally necessary for this proxy to @@ -650,21 +690,20 @@ into visiting external sites. In order to prevent this kind of attack, it's necessary to whitelist trusted proxy server IPs using the SS_TRUSTED_PROXY_IPS define in your `.env`. -``` +```bash SS_TRUSTED_PROXY_IPS="127.0.0.1,192.168.0.1" ``` You can also whitelist subnets in CIDR notation if you don't know the exact IP of a trusted proxy. For example, some cloud provider load balancers don't have fixed IPs. -``` +```bash SS_TRUSTED_PROXY_IPS="10.10.0.0/24,10.10.1.0/24,10.10.2.0/24" ``` If you wish to change the headers that are used to find the proxy information, you should reconfigure the TrustedProxyMiddleware service: - ```yml SilverStripe\Control\TrustedProxyMiddleware: properties: @@ -673,7 +712,7 @@ SilverStripe\Control\TrustedProxyMiddleware: ProxyIPHeaders: X-Forwarded-Ip ``` -``` +```bash SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST" SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR" SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_X_FORWARDED_PROTOCOL" @@ -689,7 +728,7 @@ This behaviour is enabled whenever `SS_TRUSTED_PROXY_IPS` is defined, or if the `BlockUntrustedIPs` environment variable is declared. It is advisable to include the following in your .htaccess to ensure this behaviour is activated. -``` +```text # Ensure that X-Forwarded-Host is only allowed to determine the request # hostname for servers ips defined by SS_TRUSTED_PROXY_IPS in your .env @@ -700,10 +739,10 @@ following in your .htaccess to ensure this behaviour is activated. This behaviour is on by default; the environment variable is not required. For correct operation, it is necessary to always set `SS_TRUSTED_PROXY_IPS` if using a proxy. -## Secure Sessions, Cookies and TLS (HTTPS) +## Secure sessions, cookies and TLS (HTTPS) -Silverstripe CMS recommends the use of TLS(HTTPS) for your application, and you can easily force the use through the -director function `forceSSL()` +Silverstripe CMS recommends the use of TLS (HTTPS) for your application, and you can easily force the use through the +director function `forceSSL()` ```php use SilverStripe\Control\Director; @@ -721,12 +760,13 @@ use SilverStripe\Control\Director; use SilverStripe\Control\Middleware\CanonicalURLMiddleware; if (!Director::isDev()) { - CanonicalURLMiddleware::singleton()->setEnabledEnvs(true); // You can also specify individual environment types + // You can also specify individual environment types + CanonicalURLMiddleware::singleton()->setEnabledEnvs(true); Director::forceSSL(); } ``` -Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as +Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as [lets encrypt](https://letsencrypt.org/) and configured on your web server. Note that by default enabling SSL will also enable `CanonicalURLMiddleware::forceBasicAuthToSSL` which will detect @@ -734,7 +774,7 @@ and automatically redirect any requests with basic authentication headers to fir disable this behaviour using `CanonicalURLMiddleware::singleton()->setForceBasicAuthToSSL(false)`, or via Injector configuration in YAML. -We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell Silverstripe CMS to +We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell Silverstripe CMS to use a [secure session](/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie). To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`. @@ -766,21 +806,27 @@ There is not currently an easy way to pass a `samesite` attribute value for sett default value for the attribute for all cookies. See [the main cookies documentation](/developer_guides/cookies_and_sessions/cookies#samesite-attribute) for more information. [/info] -For other cookies set by your application we should also ensure the users are provided with secure cookies by setting -the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through javascript. +For other cookies set by your application we should also ensure the users are provided with secure cookies by setting +the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through JavaScript. - - The `Secure` cookie flag instructs the browser not to send the cookie over an insecure HTTP connection. If this -flag is not present, the browser will send the cookie even if HTTPS is not in use, which means it is transmitted in +- The `Secure` cookie flag instructs the browser not to send the cookie over an insecure HTTP connection. If this +flag is not present, the browser will send the cookie even if HTTPS is not in use, which means it is transmitted in clear text and can be intercepted and stolen by an attacker who is listening on the network. -- The `HTTPOnly` flag lets the browser know whether or not a cookie should be accessible by client-side JavaScript -code. It is best practice to set this flag unless the application is known to use JavaScript to access these cookies +- The `HTTPOnly` flag lets the browser know whether or not a cookie should be accessible by client-side JavaScript +code. It is best practice to set this flag unless the application is known to use JavaScript to access these cookies as this prevents an attacker who achieves cross-site scripting from accessing these cookies. ```php use SilverStripe\Control\Cookie; -Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true, +Cookie::set( + 'cookie-name', + 'chocolate-chip', + $expiry = 30, + $path = null, + $domain = null, + $secure = true, $httpOnly = false ); ``` @@ -797,53 +843,55 @@ You can configure that by setting the following environment variables: | `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file (optional) | | `SS_DATABASE_SSL_CIPHER` | Custom SSL cipher for database connections (optional) | -## Security Headers +## Security headers -In addition to forcing HTTPS browsers can support additional security headers which can only allow access to a website -via a secure connection. As browsers increasingly provide negative feedback regarding unencrypted HTTP connections, -ensuring an HTTPS connection will provide a better and more secure user experience. +In addition to forcing HTTPS browsers can support additional security headers which can only allow access to a website +via a secure connection. As browsers increasingly provide negative feedback regarding unencrypted HTTP connections, +ensuring an HTTPS connection will provide a better and more secure user experience. -- The `Strict-Transport-Security` header instructs the browser to record that the website and assets on that website -MUST use a secure connection. This prevents websites from becoming insecure in the future from stray absolute links +- The `Strict-Transport-Security` header instructs the browser to record that the website and assets on that website +MUST use a secure connection. This prevents websites from becoming insecure in the future from stray absolute links or references without https from external sites. Check if your browser supports [HSTS](https://hsts.badssl.com/) -- `max-age` can be configured to anything in seconds: `max-age=31536000` (1 year), for roll out, consider something +- `max-age` can be configured to anything in seconds: `max-age=31536000` (1 year), for roll out, consider something lower - `includeSubDomains` to ensure all present and future sub domains will also be HTTPS For sensitive pages, such as members areas, or places where sensitive information is present, adding cache control - headers can explicitly instruct browsers not to keep a local cached copy of content and can prevent content from - being cached throughout the infrastructure (e.g. Proxy, caching layers, WAF etc). - -- The headers `Cache-control: no-store` and `Pragma: no-cache` along with expiry headers of `Expires: ` -and `Date: ` will ensure that sensitive content is not stored locally or able to be retrieved by -unauthorised local persons. Silverstripe CMS adds the current date for every request, and we can add the other cache + headers can explicitly instruct browsers not to keep a local cached copy of content and can prevent content from + being cached throughout the infrastructure (e.g. Proxy, caching layers, WAF etc). + +- The headers `Cache-control: no-store` and `Pragma: no-cache` along with expiry headers of `Expires: ` +and `Date: ` will ensure that sensitive content is not stored locally or able to be retrieved by +unauthorised local persons. Silverstripe CMS adds the current date for every request, and we can add the other cache headers to the request for our secure controllers: - + ```php -use SilverStripe\Control\HTTP; +namespace App\Control; + use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTP; -class MySecureController extends Controller +class MySecureController extends Controller { - - public function init() + public function init() { parent::init(); - + // Add cache headers to ensure sensitive content isn't cached. $this->response->addHeader('Cache-Control', 'max-age=0, must-revalidate, no-transform'); - $this->response->addHeader('Pragma', 'no-cache'); // for HTTP 1.0 support + // for HTTP 1.0 support + $this->response->addHeader('Pragma', 'no-cache'); HTTP::set_cache_age(0); HTTP::add_cache_headers($this->response); - + // Add HSTS header to force TLS for document content $this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains'); } } ``` -## HTTP Caching Headers +## HTTP caching headers Caching is hard. If you get it wrong, private or draft content might leak to unauthenticated users. We have created an abstraction which allows you to express @@ -852,9 +900,9 @@ See [HTTP Cache Headers](/developer_guides/performance/http_cache_headers/) for details on how to apply caching safely, and read Google's [Web Fundamentals on Caching](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching). -## Related +## Related - * [Silverstripe CMS security vulnerability advisories](https://silverstripe.org/security-releases/) - * [MySQL security documentation](https://dev.mysql.com/doc/refman/8.0/en/security.html) - * [OWASP Top Ten](https://owasp.org/www-project-top-ten/) - * [OWASP List of Attacks](https://owasp.org/www-community/attacks/) +- [Silverstripe CMS security vulnerability advisories](https://silverstripe.org/security-releases/) +- [MySQL security documentation](https://dev.mysql.com/doc/refman/8.0/en/security.html) +- [OWASP Top Ten](https://owasp.org/www-project-top-ten/) +- [OWASP List of Attacks](https://owasp.org/www-community/attacks/) diff --git a/en/02_Developer_Guides/09_Security/06_Rate_Limiting.md b/en/02_Developer_Guides/09_Security/06_Rate_Limiting.md index 50eb92134..eb8f4fe51 100644 --- a/en/02_Developer_Guides/09_Security/06_Rate_Limiting.md +++ b/en/02_Developer_Guides/09_Security/06_Rate_Limiting.md @@ -4,7 +4,7 @@ summary: Silverstripe CMS's in built rate limiting features icon: tachometer-alt --- -# Rate Limiting +# Rate limiting Silverstripe CMS comes with a [Middleware](../controllers/middlewares/) that provides rate limiting for the Security controller. This provides added protection to a potentially vulnerable part of a Silverstripe CMS application @@ -70,7 +70,7 @@ SilverStripe\Core\Injector\Injector: SiteWideRateLimitMiddleware: '%$SiteWideRateLimitMiddleware' ``` -## Disabling the Rate Limiter +## Disabling the rate limiter You may already solve the rate limiting problem on a server level and the built in rate limiting may well be redundant. If this is the case you can turn off the rate limiting middleware by redefining the URL rules for the Security controller. diff --git a/en/02_Developer_Guides/09_Security/07_Personal_Data.md b/en/02_Developer_Guides/09_Security/07_Personal_Data.md index 0c614c1e6..6ce131bb7 100644 --- a/en/02_Developer_Guides/09_Security/07_Personal_Data.md +++ b/en/02_Developer_Guides/09_Security/07_Personal_Data.md @@ -4,7 +4,7 @@ summary: How the Silverstripe CMS deals with data privacy icon: user-ninja --- -# Personal Data +# Personal data Silverstripe CMS is an application framework which can be used to process and store data. Any data can be sensitive, particularly if it is @@ -28,7 +28,7 @@ or cover this through other contractual arrangements with the individuals. The primary location where Silverstripe CMS can be configured to store personal data is the database. Under different regulations, individuals can have the "right to be forgotten", -and can ask website operators to remove their data. +and can ask website operators to remove their data. Most of the time, CMS administrators can action this without any technical help through the CMS (through the “Security” section, or specialised UIs like user defined forms). @@ -36,7 +36,7 @@ Be careful with Versioned records containing personal data: These might require development effort to completely remove. Note that CMS users aren’t versioned by default, so you can completely remove them through the UI. -## Transmission and Processing +## Transmission and processing Silverstripe CMS recommends the use of encryption in transit (e.g. TLS/SSL), and at rest (e.g. database encryption), but does not enforce these. @@ -46,11 +46,11 @@ and at rest (e.g. database encryption), but does not enforce these. Silverstripe CMS will default to using PHP sessions for tracking logged-in users, which uniquely link users to their device/browser through a session cookie. If the user chooses the "Remember me" feature on login, -this unique link will persist across sessions. +this unique link will persist across sessions. The default cookie lifetime for this feature is 48h. See `SilverStripe\Security\Member::$auto_login_token_lifetime` for details. -## Login Attempts +## Login attempts Silverstripe CMS is configured by default to record login attempts, in order to lock out users after a defined number of attempts, and hence limit the attack surface of the login process. @@ -60,7 +60,7 @@ from the `LoginAttempt` table. See `SilverStripe\Security\Security::$login_recording` and `SilverStripe\Security\Security::$lock_out_after_incorrect_logins` for details. -## Logging and Exceptions +## Logging and exceptions Silverstripe CMS provides a logging mechanism, which depending on your usage, configuration and hosting environment might store personal data outside of the Silverstripe CMS database. @@ -69,6 +69,6 @@ The core system stores personal data for members, but does not log it. As a PHP application, Silverstripe CMS can also throw exceptions. These can include metadata such as method arguments and session data. If your application is configured to catch exceptions and log them (e.g. via a SaaS product), you could inadvertently store -personal data in other systems. One mitigation is to create whitelists based on +personal data in other systems. One mitigation is to create whitelists based on parameter naming, see the [silverstripe/raygun](https://github.com/silverstripe/silverstripe-raygun) -module for an example implementation. +module for an example implementation. diff --git a/en/02_Developer_Guides/09_Security/08_SQL_Placeholders.md b/en/02_Developer_Guides/09_Security/08_SQL_Placeholders.md index b4bfa5b89..0cd3b154f 100644 --- a/en/02_Developer_Guides/09_Security/08_SQL_Placeholders.md +++ b/en/02_Developer_Guides/09_Security/08_SQL_Placeholders.md @@ -4,11 +4,11 @@ summary: SQL placeholders in ORM queries icon: tachometer-alt --- -# SQL Placeholders +# SQL placeholders SQL placeholders are `?` characters used as a placeholder for a value in a SQL query as a way to prevent SQL injection attacks. They are used by default extensively in queries created by the ORM. -For increased performance, placeholders are not used when filtering by an array of integer only values on a column that is either a [`DBPrimarykey`](api:SilverStripe\ORM\FieldType\DBPrimaryKey) or a [`DBForiegnKey`](api:SilverStripe\ORM\FieldType\DBForiegnKey). An example of this type of ORM filter is `->filter(['ID' => $ids])` which will turn into a SQL containing `WHERE IN ()`. +For increased performance, placeholders are not used when filtering by an array of integer only values on a column that is either a [`DBPrimarykey`](api:SilverStripe\ORM\FieldType\DBPrimaryKey) or a [`DBForiegnKey`](api:SilverStripe\ORM\FieldType\DBForiegnKey). An example of this type of ORM filter is `->filter(['ID' => $ids])` which will turn into a SQL containing `WHERE IN ()`. There is no chance of SQL injection because of the exclusive use of integers for values. However, if you still wish for placeholders to be used for this type of query then you can enable them with the following config: diff --git a/en/02_Developer_Guides/09_Security/index.md b/en/02_Developer_Guides/09_Security/index.md index c7f46c32a..fa1d5463f 100644 --- a/en/02_Developer_Guides/09_Security/index.md +++ b/en/02_Developer_Guides/09_Security/index.md @@ -4,9 +4,9 @@ summary: This guide covers user authentication, the permission system and how to icon: user-shield --- -# Security and User Authentication +# Security and user authentication -This guide covers using and extending the user authentication in Silverstripe CMS, permissions, user groups and roles, and +This guide covers using and extending the user authentication in Silverstripe CMS, permissions, user groups and roles, and how to secure your code against malicious behaviors of both your users and hackers. [CHILDREN Exclude=How_to] diff --git a/en/02_Developer_Guides/10_Email/index.md b/en/02_Developer_Guides/10_Email/index.md index 8d2a0e57e..1e9d6ee5e 100644 --- a/en/02_Developer_Guides/10_Email/index.md +++ b/en/02_Developer_Guides/10_Email/index.md @@ -17,21 +17,22 @@ Email configuration is done using Symfony's [DSN](https://symfony.com/doc/curren The `Sendmail` transport is the most common one and is used by default in Silverstripe. The `sendmail` binary is widely available across most Linux/Unix servers. By default the sendmail command used is `/usr/sbin/sendmail -bs`, but this [can be configured](#dsn-sendmail) as part of the `DSN`. Alternatively you can provide a different `DSN` to select any of the Transport classes provided natively by `symfony/mailer` or other compatible third-party transports. For more information and to see what other transports are available see the [symfony/mailer transport types](https://symfony.com/doc/current/mailer.html#using-a-3rd-party-transport). + [hint] The format for the DSN is exactly as defined in the symfony docs linked above. Some common examples are listed below. [/hint] To set the DSN string in an environment variable (recommended): -**.env** -``` +```bash +# .env MAILER_DSN="" ``` To set the DSN string in project yml: -**app/_config/mailer-project.yml** ```yml +# app/_config/mailer-project.yml --- Name: mailer-project After: 'mailer' @@ -43,30 +44,31 @@ SilverStripe\Core\Injector\Injector: ``` The configuration priority order is as follows, from highest to lowest: + - The `MAILER_DSN` environment variable - Project yml containing `After: 'mailer'` -- The default silverstripe DSN of `sendmail://default` which will use `/usr/sbin/sendmail -bs` +- The default DSN of `sendmail://default` which will use `/usr/sbin/sendmail -bs` ### Common DSN strings #### To configure SMTP {#dsn-smtp} -**.env** -``` +```bash +# .env MAILER_DSN="smtp://user:pass@smtp.example.com:1234" ``` #### To configure a different sendmail binary and command {#dsn-sendmail} -**.env** -``` +```bash +# .env MAILER_DSN="sendmail://default?command=/path/to/mysendmailbinary%20-t" ``` #### To suppress all emails {#dsn-null} -**.env** -``` +```bash +# .env MAILER_DSN="null://default" ``` @@ -74,7 +76,7 @@ Read more about other available DSN strings in [the symfony documentation](https ### Testing that email works -You _must_ ensure emails are being sent from your _production_ environment. You can do this by testing that the +You *must* ensure emails are being sent from your *production* environment. You can do this by testing that the ***Lost password*** form available at `/Security/lostpassword` sends an email to your inbox, or with the following code snippet that can be run via a `SilverStripe\Dev\BuildTask`: ```php @@ -99,8 +101,8 @@ $email->sendPlain(); ### Sending combined HTML and plain text -By default, emails are sent in both HTML and Plaintext format. A plaintext representation is automatically generated -from the system by stripping HTML markup, or transforming it where possible (e.g. `text` is converted +By default, emails are sent in both HTML and Plaintext format. A plaintext representation is automatically generated +from the system by stripping HTML markup, or transforming it where possible (e.g. `text` is converted to `*text*`). You can also specify plain text and HTML content separately if you don't want the plain text to be automatically generated from HTML @@ -116,18 +118,17 @@ $email->send(); [info] The default HTML template for emails is `vendor/silverstripe/framework/templates/SilverStripe/Control/Email/Email.ss`. -To customise this template, first copy it to `/themes//SilverStripe/Control/Email/Email.ss`. Alternatively, copy it to a different location and use `setHTMLTemplate` when you create the +To customise this template, first copy it to `/themes//SilverStripe/Control/Email/Email.ss`. Alternatively, copy it to a different location and use `setHTMLTemplate` when you create the `Email` instance. Note - by default the `$EmailContent` variable will escape HTML tags for security reasons. If you feel confident allowing this variable to be rendered as HTML, then update your custom email template to `$EmailContent.RAW` [/info] ### Templates HTML emails can use custom templates using the same template language as your website template. You can also pass the -email object additional information using the `setData` and `addData` methods. - -**app/templates/Email/MyCustomEmail.ss** +email object additional information using the `setData` and `addData` methods. ```ss +<%-- app/templates/Email/MyCustomEmail.ss --%>

          Hi $Member.FirstName

          You can go to $Link.

          ``` @@ -135,14 +136,14 @@ email object additional information using the `setData` and `addData` methods. The PHP Logic.. ```php - use SilverStripe\Control\Email\Email; +use SilverStripe\Security\Security; $email = Email::create() - ->setHTMLTemplate('Email\\MyCustomEmail') + ->setHTMLTemplate('Email\\MyCustomEmail') ->setData([ 'Member' => Security::getCurrentUser(), - 'Link'=> $link, + 'Link' => $link, ]) ->from($from) ->to($to) @@ -174,19 +175,19 @@ $email->setPlainTemplate('MyPlanTemplate'); $email->send(); ``` -## Administrator Emails +## Administrator emails You can set the default sender address of emails through the `Email.admin_email` [configuration setting](/developer_guides/configuration). -**app/_config/app.yml** -```yaml +```yml +# app/_config/app.yml SilverStripe\Control\Email\Email: admin_email: support@example.com ``` To add a display name, set `admin_email` as follow. -```yaml +```yml SilverStripe\Control\Email\Email: admin_email: support@example.com: 'Support team' @@ -198,7 +199,7 @@ SilverStripe\Control\Email\Email: use SilverStripe\Control\Email\Email; $from = [ - 'from@mysite.exmaple.com' => 'Friendly business' + 'from@mysite.exmaple.com' => 'Friendly business', ]; $to = [ 'person.a@customer.example.com' => 'Person A', @@ -208,7 +209,6 @@ $to = [ $email = Email::create($from, $to, $subject, $body); ``` - [alert] Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your email marked as spam. If you want to send from another address think about using the `setReplyTo` method. @@ -218,36 +218,37 @@ You will also have to remove the `SS_SEND_ALL_EMAILS_FROM` environment variable If you need greater control over this email address, for instance if are running the subsites modules, you can implement the `SilverStripe\Control\Email\Email::updateDefaultFrom()` extension hook. -## Redirecting Emails +## Redirecting emails There are several other [configuration settings](/developer_guides/configuration) to manipulate the email server. -* `SilverStripe\Control\Email\Email.send_all_emails_to` will redirect all emails sent to the given address. -All recipients will be removed (including CC and BCC addresses). This is useful for testing and staging servers where +- `SilverStripe\Control\Email\Email.send_all_emails_to` will redirect all emails sent to the given address. +All recipients will be removed (including CC and BCC addresses). This is useful for testing and staging servers where you do not wish to send emails out. For debugging the original addresses are added as `X-Original-*` headers on the email. -* `SilverStripe\Control\Email\Email.cc_all_emails_to` and `SilverStripe\Control\Email\Email.bcc_all_emails_to` will add -an additional recipient in the BCC / CC header. These are good for monitoring system-generated correspondence on the +- `SilverStripe\Control\Email\Email.cc_all_emails_to` and `SilverStripe\Control\Email\Email.bcc_all_emails_to` will add +an additional recipient in the BCC / CC header. These are good for monitoring system-generated correspondence on the live systems. Configuration of those properties looks like the following: -**app/_config.php** ```php +// app/_config.php +use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; use SilverStripe\Core\Config\Config; -if(Director::isLive()) { - Config::modify()->set(Email::class, 'bcc_all_emails_to', "client@example.com"); +if (Director::isLive()) { + Config::modify()->set(Email::class, 'bcc_all_emails_to', 'client@example.com'); } else { - Config::modify()->set(Email::class, 'send_all_emails_to', "developer@example.com"); + Config::modify()->set(Email::class, 'send_all_emails_to', 'developer@example.com'); } ``` -### Setting custom "Reply To" email address. +### Setting custom "Reply To" email address -For email messages that should have an email address which is replied to that actually differs from the original "from" +For email messages that should have an email address which is replied to that actually differs from the original "from" email, do the following. This is encouraged especially when the domain responsible for sending the message isn't -necessarily the same which should be used for return correspondence and should help prevent your message from being +necessarily the same which should be used for return correspondence and should help prevent your message from being marked as spam. ```php @@ -280,6 +281,4 @@ For more information, refer to [handling sending failures](https://symfony.com/d Silverstripe Email is built on top of [symfony/mailer](https://github.com/symfony/mailer). For advanced customisation information refer to the [symfony/mailer docs](https://symfony.com/doc/current/mailer.html) -## API Documentation - -* [Email](api:SilverStripe\Control\Email\Email) +- [Email](api:SilverStripe\Control\Email\Email) diff --git a/en/02_Developer_Guides/11_Integration/00_CSV_Import.md b/en/02_Developer_Guides/11_Integration/00_CSV_Import.md index 655a017c3..3f3804348 100644 --- a/en/02_Developer_Guides/11_Integration/00_CSV_Import.md +++ b/en/02_Developer_Guides/11_Integration/00_CSV_Import.md @@ -3,6 +3,7 @@ title: CSV Import summary: Load data into your Silverstripe CMS database in bulk icon: upload --- + # Import CSV data ## Introduction @@ -12,27 +13,28 @@ but this method doesn't know anything about your datamodel. In Silverstripe CMS, this can be handled through the a specialized CSV importer class that can be customised to fit your data. -## The CsvBulkLoader class +## The `CsvBulkLoader` class The [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) class facilitate complex CSV-imports by defining column-mappings and custom converters. It uses PHP's built-in `fgetcsv()` function to process CSV input, and accepts a file handle as an input. Feature overview: -* Custom column mapping -* Auto-detection of CSV-header rows -* Duplicate detection based on custom criteria -* Automatic generation of relations based on one or more columns in the CSV-Data -* Definition of custom import methods (e.g. for date conversion or combining multiple columns) -* Optional deletion of existing records if they're not present in the CSV-file -* Results grouped by "imported", "updated" and "deleted" +- Custom column mapping +- Auto-detection of CSV-header rows +- Duplicate detection based on custom criteria +- Automatic generation of relations based on one or more columns in the CSV-Data +- Definition of custom import methods (e.g. for date conversion or combining multiple columns) +- Optional deletion of existing records if they're not present in the CSV-file +- Results grouped by "imported", "updated" and "deleted" ## Usage You can use the CsvBulkLoader without subclassing or other customizations, if the column names -in your CSV file match `$db` properties in your dataobject. E.g. a simple import for the +in your CSV file match `$db` properties in your dataobject. For example a simple import for the [Member](api:SilverStripe\Security\Member) class could have this data in a file: -``` + +```text FirstName,LastName,Email Donald,Duck,donald@disney.com Daisy,Duck,daisy@disney.com @@ -40,35 +42,38 @@ Daisy,Duck,daisy@disney.com The loader would be triggered through the `load()` method: - ```php use SilverStripe\Dev\CsvBulkLoader; -$loader = new CsvBulkLoader('Member'); +$loader = CsvBulkLoader::create('Member'); $result = $loader->load(''); ``` By the way, you can import [Member](api:SilverStripe\Security\Member) and [Group](api:SilverStripe\Security\Group) data through `https://www.example.com/admin/security` interface out of the box. -## Import through ModelAdmin +## Import through `ModelAdmin` The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is through a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) interface - you get an upload form out of the box. - ```php +namespace App\Admin; + +use App\Model\Player; use SilverStripe\Admin\ModelAdmin; use SilverStripe\Dev\CsvBulkLoader; -class PlayerAdmin extends ModelAdmin +class PlayerAdmin extends ModelAdmin { - private static $managed_models = [ - Player::class - ]; - private static $model_importers = [ + private static $managed_models = [ + Player::class, + ]; + + private static $model_importers = [ Player::class => CsvBulkLoader::class, - ]; - private static $url_segment = 'players'; + ]; + + private static $url_segment = 'players'; } ``` @@ -77,56 +82,68 @@ below the search form on the left. ## Import through a custom controller -You can have more customised logic and interface feedback through a custom controller. -Let's create a simple upload form (which is used for `MyDataObject` instances). -You'll need to add a route to your controller to make it accessible via URL +You can have more customised logic and interface feedback through a custom controller. +Let's create a simple upload form (which is used for `MyDataObject` instances). +You'll need to add a route to your controller to make it accessible via URL (see [Routing](../../controllers/routing/)). - ```php -use SilverStripe\Forms\Form; +namespace App\Control; + +use App\Model\MyDataObject; +use SilverStripe\Control\Controller; +use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FileField; +use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; -use SilverStripe\Dev\CsvBulkLoader; -use SilverStripe\Control\Controller; -class MyController extends Controller +class MyController extends Controller { + private static $url_segment = 'my_controller'; - private static $allowed_actions = ['Form']; + private static $allowed_actions = [ + 'getForm', + ]; - protected $template = "BlankPage"; + private static $url_handlers = [ + 'Form' => 'getForm', + ]; - public function Link($action = null) - { - return Controller::join_links('MyController', $action); - } + protected $template = 'BlankPage'; - public function Form() + public function getForm() { - $form = new Form( + $form = Form::create( $this, 'Form', - new FieldList( - new FileField('CsvFile', false) + FieldList::create( + FileField::create('CsvFile', false) ), - new FieldList( - new FormAction('doUpload', 'Upload') + FieldList::create( + FormAction::create('doUpload', 'Upload') ) ); return $form; } - public function doUpload($data, $form) + public function doUpload($data, $form) { - $loader = new CsvBulkLoader('MyDataObject'); + $loader = CsvBulkLoader::create(MyDataObject::class); $results = $loader->load($_FILES['CsvFile']['tmp_name']); $messages = []; - if($results->CreatedCount()) $messages[] = sprintf('Imported %d items', $results->CreatedCount()); - if($results->UpdatedCount()) $messages[] = sprintf('Updated %d items', $results->UpdatedCount()); - if($results->DeletedCount()) $messages[] = sprintf('Deleted %d items', $results->DeletedCount()); - if(!$messages) $messages[] = 'No changes'; + if ($results->CreatedCount()) { + $messages[] = sprintf('Imported %d items', $results->CreatedCount()); + } + if ($results->UpdatedCount()) { + $messages[] = sprintf('Updated %d items', $results->UpdatedCount()); + } + if ($results->DeletedCount()) { + $messages[] = sprintf('Deleted %d items', $results->DeletedCount()); + } + if (!$messages) { + $messages[] = 'No changes'; + } $form->sessionMessage(implode(', ', $messages), 'good'); return $this->redirectBack(); @@ -142,7 +159,8 @@ with certain access rights. We're going to use our knowledge from the previous example to import a more sophisticated CSV file. Sample CSV Content -``` + +```text "Number","Name","Birthday","Team" 11,"John Doe",1982-05-12,"FC Bayern" 12,"Jane Johnson", 1982-05-12,"FC Bayern" @@ -151,99 +169,121 @@ Sample CSV Content Datamodel for Player - ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { - private static $db = [ + private static $db = [ 'PlayerNumber' => 'Int', 'FirstName' => 'Text', 'LastName' => 'Text', 'Birthday' => 'Date', - ]; - private static $has_one = [ - 'Team' => 'FootballTeam' - ]; + ]; + + private static $has_one = [ + 'Team' => FootballTeam::class, + ]; } ``` Datamodel for FootballTeam: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class FootballTeam extends DataObject +class FootballTeam extends DataObject { - private static $db = [ + private static $db = [ 'Title' => 'Text', - ]; - private static $has_many = [ - 'Players' => 'Player' - ]; + ]; + + private static $has_many = [ + 'Players' => Player::class, + ]; } ``` Sample implementation of a custom loader. Assumes a CSV-file in a certain format (see below). -* Converts property names -* Splits a combined "Name" fields from the CSV-data into `FirstName` and `Lastname` by a custom importer method -* Avoids duplicate imports by a custom `$duplicateChecks` definition -* Creates `Team` relations automatically based on the `Gruppe` column in the CSV data +- Converts property names +- Splits a combined "Name" fields from the CSV-data into `FirstName` and `Lastname` by a custom importer method +- Avoids duplicate imports by a custom `$duplicateChecks` definition +- Creates `Team` relations automatically based on the `Gruppe` column in the CSV data ```php +namespace App\Admin; + +use App\Model\FootballTeam; use SilverStripe\Dev\CsvBulkLoader; -class PlayerCsvBulkLoader extends CsvBulkLoader +class PlayerCsvBulkLoader extends CsvBulkLoader { - public $columnMap = [ - 'Number' => 'PlayerNumber', - 'Name' => '->importFirstAndLastName', - 'Birthday' => 'Birthday', - 'Team' => 'Team.Title', - ]; - public $duplicateChecks = [ - 'Number' => 'PlayerNumber' - ]; - public $relationCallbacks = [ - 'Team.Title' => [ - 'relationname' => 'Team', - 'callback' => 'getTeamByTitle' - ] - ]; - public static function importFirstAndLastName(&$obj, $val, $record) - { - $parts = explode(' ', $val); - if(count($parts) != 2) return false; - $obj->FirstName = $parts[0]; - $obj->LastName = $parts[1]; - } - public static function getTeamByTitle(&$obj, $val, $record) - { - return FootballTeam::get()->filter('Title', $val)->First(); - } + public function __construct($objectClass) + { + $this->columnMap = [ + 'Number' => 'PlayerNumber', + 'Name' => '->importFirstAndLastName', + 'Birthday' => 'Birthday', + 'Team' => 'Team.Title', + ]; + + $this->duplicateChecks = [ + 'Number' => 'PlayerNumber', + ]; + + $this->relationCallbacks = [ + 'Team.Title' => [ + 'relationname' => 'Team', + 'callback' => 'getTeamByTitle', + ], + ]; + + parent::construct($objectClass); + } + + public static function importFirstAndLastName(&$obj, $val, $record) + { + $parts = explode(' ', $val); + if (count($parts) != 2) { + return false; + } + $obj->FirstName = $parts[0]; + $obj->LastName = $parts[1]; + } + + public static function getTeamByTitle(&$obj, $val, $record) + { + return FootballTeam::get()->filter('Title', $val)->First(); + } } ``` Building off of the ModelAdmin example up top, use a custom loader instead of the default loader by adding it to `$model_importers`. In this example, `CsvBulkLoader` is replaced with `PlayerCsvBulkLoader`. - ```php +namespace App\Admin; + use SilverStripe\Admin\ModelAdmin; -class PlayerAdmin extends ModelAdmin +class PlayerAdmin extends ModelAdmin { - private static $managed_models = [ - 'Player' - ]; - private static $model_importers = [ - 'Player' => 'PlayerCsvBulkLoader', - ]; - private static $url_segment = 'players'; + private static $managed_models = [ + 'Player', + ]; + + private static $model_importers = [ + 'Player' => PlayerCsvBulkLoader::class, + ]; + + private static $url_segment = 'players'; } ``` ## Related -* [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) +- [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) diff --git a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md index 529f3c376..0426ec105 100644 --- a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md +++ b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md @@ -4,13 +4,13 @@ summary: Output records from your database as an RSS Feed. icon: rss --- -# RSS Feed +# RSS feed Generating RSS / Atom-feeds is a matter of rendering a [SS_List](api:SilverStripe\ORM\SS_List) instance through the [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) class. -The [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) class doesn't limit you to generating article based feeds, it is just as easy to create a feed of +The [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) class doesn't limit you to generating article based feeds, it is just as easy to create a feed of your current staff members, comments or any other custom [DataObject](api:SilverStripe\ORM\DataObject) subclasses you have defined. The only -logical limitation here is that every item in the RSS-feed should be accessible through a URL on your website, so it's +logical limitation here is that every item in the RSS-feed should be accessible through a URL on your website, so it's advisable to just create feeds from subclasses of [SiteTree](api:SilverStripe\CMS\Model\SiteTree). [warning] @@ -20,16 +20,15 @@ the object. ## Usage -Including an RSS feed has two steps. First, a `Controller` action which responses with the `XML` and secondly, the other +Including an RSS feed has two steps. First, a `Controller` action which responses with the `XML` and secondly, the other web pages need to link to the URL to notify users that the RSS feed is available and where it is. An outline of step one looks like: - ```php use SilverStripe\Control\RSS\RSSFeed; -$feed = new RSSFeed( +$feed = RSSFeed::create( $list, $link, $title, @@ -58,48 +57,48 @@ RSSFeed::linkToFeed($link, $title); You can use [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) to easily create a feed showing your latest Page updates. The following example adds a page `/home/rss/` which displays an XML file the latest updated pages. -**app/src/PageController.php** - ```php +// app/src/PageType/HomePageController.php +namespace App\PageType; + +use PageController; use SilverStripe\Control\RSS\RSSFeed; -use SilverStripe\CMS\Controllers\ContentController; -class PageController extends ContentController +class HomePageController extends PageController { private static $allowed_actions = [ - 'rss' + 'rss', ]; - public function init() + public function init() { parent::init(); - RSSFeed::linkToFeed($this->Link("rss"), "10 Most Recently Updated Pages"); + RSSFeed::linkToFeed($this->Link('rss'), '10 Most Recently Updated Pages'); } - public function rss() + public function rss() { - $rss = new RSSFeed( - $this->LatestUpdates(), - $this->Link(), - "10 Most Recently Updated Pages", - "Shows a list of the 10 most recently updated pages." + $rss = RSSFeed::create( + $this->getLatestUpdates(), + $this->Link(), + '10 Most Recently Updated Pages', + 'Shows a list of the 10 most recently updated pages.' ); return $rss->outputToBrowser(); } - public function LatestUpdates() + public function getLatestUpdates() { - return Page::get()->sort("LastEdited", "DESC")->limit(10); + return HomePage::get()->sort('LastEdited', 'DESC')->limit(10); } } - ``` -### Rendering DataObjects in a RSSFeed +### Rendering `DataObject` records in a RSS feed -DataObjects can be rendered in the feed as well, however, since they aren't explicitly [SiteTree](api:SilverStripe\CMS\Model\SiteTree) subclasses we +DataObjects can be rendered in the feed as well, however, since they aren't explicitly [SiteTree](api:SilverStripe\CMS\Model\SiteTree) subclasses we need to include a function `AbsoluteLink` to allow the RSS feed to link through to the item. [info] @@ -109,19 +108,18 @@ If the items are all displayed on a single page you may simply hard code the lin Take an example, we want to create an RSS feed of all the `Players` objects in our site. We make sure the `AbsoluteLink` method is defined and returns a string to the full website URL. - ```php +namespace App\Model; + use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { - - public function AbsoluteLink() + public function AbsoluteLink() { // assumes players can be accessed at www.example.com/players/2 - return Controller::join_links( Director::absoluteBaseUrl(), 'players', @@ -131,33 +129,34 @@ class Player extends DataObject } ``` -Then in our controller, we add a new action which returns a the XML list of `Players`. - +Then in our controller, we add a new action which returns a the XML list of `Player` records. ```php +namespace App\PageType; + +use App\Model\Player; +use PageController; use SilverStripe\Control\RSS\RSSFeed; -use SilverStripe\CMS\Controllers\ContentController; -class PageController extends ContentController +class HomePageController extends PageController { - private static $allowed_actions = [ - 'players' + 'players', ]; - public function init() + public function init() { parent::init(); - RSSFeed::linkToFeed($this->Link("players"), "Players"); + RSSFeed::linkToFeed($this->Link('players'), 'Players'); } - public function players() + public function players() { - $rss = new RSSFeed( + $rss = RSSFeed::create( Player::get(), - $this->Link("players"), - "Players" + $this->Link('players'), + 'Players' ); return $rss->outputToBrowser(); @@ -165,16 +164,15 @@ class PageController extends ContentController } ``` -### Customizing the RSS Feed template +### Customizing the RSS feed template -The default template used for XML view is `vendor/silverstripe/framework/templates/RSSFeed.ss`. This template displays titles and links to +The default template used for XML view is `vendor/silverstripe/framework/templates/RSSFeed.ss`. This template displays titles and links to the object. To customise the XML produced use `setTemplate`. Say from that last example we want to include the Players Team in the XML feed we might create the following XML file. -**app/templates/PlayersRss.ss** - ```xml + @@ -193,24 +191,29 @@ Say from that last example we want to include the Players Team in the XML feed w ``` -`setTemplate` can then be used to tell RSSFeed to use that new template. - -**app/src/Page.php** +`setTemplate` can then be used to tell RSSFeed to use that new template. ```php +// app/src/PageType/HomePage.php +namespace App\PageType; + +use Page; use SilverStripe\Control\RSS\RSSFeed; -public function players() +class HomePage extends Page { - $rss = new RSSFeed( - Player::get(), - $this->Link("players"), - "Players" - ); + public function players() + { + $rss = RSSFeed::create( + Player::get(), + $this->Link('players'), + 'Players' + ); - $rss->setTemplate('PlayersRss'); + $rss->setTemplate('PlayersRss'); - return $rss->outputToBrowser(); + return $rss->outputToBrowser(); + } } ``` @@ -218,7 +221,6 @@ public function players() As we've added a new template (PlayersRss.ss) make sure you clear your Silverstripe CMS cache. [/warning] +## API documentation -## API Documentation - -* [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) +- [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) diff --git a/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md b/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md index c79d62eb1..d4cc1511a 100644 --- a/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md +++ b/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md @@ -4,71 +4,73 @@ summary: Data importing through the frontend icon: upload --- -# Import CSV Data through a Controller +# Import CSV data through a controller -You can have more customised logic and interface feedback through a custom controller. Let's create a simple upload -form (which is used for `MyDataObject` instances). You can access it through +You can have more customised logic and interface feedback through a custom controller. Let's create a simple upload +form (which is used for `MyDataObject` instances). You can access it through `https://www.example.com/MyController/?flush=all`. - ```php -use SilverStripe\Forms\Form; +namespace App\Control; + +use App\Model\MyDataObject; +use SilverStripe\Control\Controller; +use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\FieldsValidator; use SilverStripe\Forms\FileField; +use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; -use SilverStripe\Forms\FieldsValidator; -use SilverStripe\Dev\CsvBulkLoader; -use SilverStripe\Control\Controller; -class MyController extends Controller +class MyController extends Controller { + private static $url_segment = 'my_controller'; private static $allowed_actions = [ - 'Form' + 'getForm', ]; - protected $template = "BlankPage"; + private static $url_handlers = [ + 'Form' => 'getForm', + ]; - public function Link($action = null) - { - return Controller::join_links('MyController', $action); - } + protected $template = 'BlankPage'; - public function Form() + public function getForm() { - $form = new Form( + $form = Form::create( $this, 'Form', - new FieldList( - new FileField('CsvFile', false) + FieldList::create( + FileField::create('CsvFile', false) ), - new FieldList( - new FormAction('doUpload', 'Upload') + FieldList::create( + FormAction::create('doUpload', 'Upload') ), - new FieldsValidator() + FieldsValidator::create() ); return $form; } - public function doUpload($data, $form) + public function doUpload($data, $form) { - $loader = new CsvBulkLoader('MyDataObject'); + $loader = CsvBulkLoader::create(MyDataObject::class); $results = $loader->load($_FILES['CsvFile']['tmp_name']); $messages = []; - if($results->CreatedCount()) { + if ($results->CreatedCount()) { $messages[] = sprintf('Imported %d items', $results->CreatedCount()); } - if($results->UpdatedCount()) { + if ($results->UpdatedCount()) { $messages[] = sprintf('Updated %d items', $results->UpdatedCount()); } - if($results->DeletedCount()) { + if ($results->DeletedCount()) { $messages[] = sprintf('Deleted %d items', $results->DeletedCount()); } - if(!$messages) { + if (!$messages) { $messages[] = 'No changes'; } @@ -80,6 +82,6 @@ class MyController extends Controller ``` [alert] -This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users with certain +This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users with certain access rights. [/alert] diff --git a/en/02_Developer_Guides/11_Integration/How_Tos/custom_csvbulkloader.md b/en/02_Developer_Guides/11_Integration/How_Tos/custom_csvbulkloader.md index a1a92fa2c..85f16b083 100644 --- a/en/02_Developer_Guides/11_Integration/How_Tos/custom_csvbulkloader.md +++ b/en/02_Developer_Guides/11_Integration/How_Tos/custom_csvbulkloader.md @@ -4,115 +4,121 @@ summary: Customise your data importing icon: upload --- -# How to: A custom CSVBulkLoader instance +# How to: a custom `CSVBulkLoader` instance -A an implementation of a custom `CSVBulkLoader` loader. In this example. we're provided with a unique CSV file +A an implementation of a custom `CSVBulkLoader` loader. In this example. we're provided with a unique CSV file containing a list of football players and the team they play for. The file we have is in the format like below. -``` +```text "SpielerNummer", "Name", "Geburtsdatum", "Gruppe" 11, "John Doe", 1982-05-12,"FC Bayern" 12, "Jane Johnson", 1982-05-12,"FC Bayern" 13, "Jimmy Dole",,"Schalke 04" ``` -This data needs to be imported into our application. For this, we have two `DataObjects` setup. `Player` contains -information about the individual player and a relation set up for managing the `Team`. - - **app/src/Player.php**. - +This data needs to be imported into our application. For this, we have two `DataObjects` setup. `Player` contains +information about the individual player and a relation set up for managing the `Team`. ```php +// app/src/Model/Player.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { - - private static $db = [ + private static $db = [ 'PlayerNumber' => 'Int', 'FirstName' => 'Text', 'LastName' => 'Text', - 'Birthday' => 'Date' - ]; - - private static $has_one = [ - 'Team' => 'FootballTeam' - ]; + 'Birthday' => 'Date', + ]; + + private static $has_one = [ + 'Team' => FootballTeam::class, + ]; } ``` -**app/src/FootballTeam.php** - - ```php +// app/src/Model/FootballTeam.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class FootballTeam extends DataObject -{ - private static $db = [ - 'Title' => 'Text' - ]; +class FootballTeam extends DataObject +{ + private static $db = [ + 'Title' => 'Text', + ]; - private static $has_many = [ - 'Players' => 'Player' - ]; + private static $has_many = [ + 'Players' => Player::class, + ]; } ``` -Now going back to look at the CSV, we can see that what we're provided with does not match what our data model looks +Now going back to look at the CSV, we can see that what we're provided with does not match what our data model looks like, so we have to create a sub class of `CsvBulkLoader` to handle the unique file. Things we need to consider with the custom importer are: -* Convert property names (e.g Number to PlayerNumber) through providing a `$columnMap`. -* Split a combined "Name" field into `FirstName` and `LastName` by calling `importFirstAndLastName` on the `Name` +- Convert property names (e.g. Number to PlayerNumber) through providing a `$columnMap`. +- Split a combined "Name" field into `FirstName` and `LastName` by calling `importFirstAndLastName` on the `Name` column -* Prevent duplicate imports by a custom `$duplicateChecks` definition. -* Create a `Team` automatically based on the `Gruppe` column and a entry for `$relationCallbacks` +- Prevent duplicate imports by a custom `$duplicateChecks` definition. +- Create a `Team` automatically based on the `Gruppe` column and a entry for `$relationCallbacks` Our final import looks like this. -**app/src/PlayerCsvBulkLoader.php** - - ```php +// app/src/Admin/PlayerCsvBulkLoader.php +namespace App\Admin; + +use App\Model\FootballTeam; use SilverStripe\Dev\CsvBulkLoader; -class PlayerCsvBulkLoader extends CsvBulkLoader +class PlayerCsvBulkLoader extends CsvBulkLoader { - - public $columnMap = [ - 'Number' => 'PlayerNumber', - 'Name' => '->importFirstAndLastName', - 'Geburtsdatum' => 'Birthday', - 'Gruppe' => 'Team.Title', - ]; - - public $duplicateChecks = [ - 'SpielerNummer' => 'PlayerNumber' - ]; - - public $relationCallbacks = [ - 'Team.Title' => [ - 'relationname' => 'Team', - 'callback' => 'getTeamByTitle' - ] - ]; - - public static function importFirstAndLastName(&$obj, $val, $record) - { - $parts = explode(' ', $val); - if(count($parts) != 2) return false; - $obj->FirstName = $parts[0]; - $obj->LastName = $parts[1]; - } - - public static function getTeamByTitle(&$obj, $val, $record) - { - return FootballTeam::get()->filter('Title', $val)->First(); - } + public function __construct($objectClass) + { + $this->columnMap = [ + 'Number' => 'PlayerNumber', + 'Name' => '->importFirstAndLastName', + 'Geburtsdatum' => 'Birthday', + 'Gruppe' => 'Team.Title', + ]; + + $this->duplicateChecks = [ + 'SpielerNummer' => 'PlayerNumber', + ] + + $this->relationCallbacks = [ + 'Team.Title' => [ + 'relationname' => 'Team', + 'callback' => 'getTeamByTitle', + ], + ]; + + parent::construct($objectClass); + } + + public static function importFirstAndLastName(&$obj, $val, $record) + { + $parts = explode(' ', $val); + if (count($parts) != 2) { + return false; + } + $obj->FirstName = $parts[0]; + $obj->LastName = $parts[1]; + } + + public static function getTeamByTitle(&$obj, $val, $record) + { + return FootballTeam::get()->filter('Title', $val)->First(); + } } ``` ## Related -* [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) +- [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) diff --git a/en/02_Developer_Guides/11_Integration/How_Tos/index.md b/en/02_Developer_Guides/11_Integration/How_Tos/index.md index bfa3fa6e1..8101dd085 100644 --- a/en/02_Developer_Guides/11_Integration/How_Tos/index.md +++ b/en/02_Developer_Guides/11_Integration/How_Tos/index.md @@ -1,6 +1,6 @@ --- title: How To's --- -# How To's: Integration and Web Services +# How to's: integration and web services -[CHILDREN] \ No newline at end of file +[CHILDREN] diff --git a/en/02_Developer_Guides/11_Integration/index.md b/en/02_Developer_Guides/11_Integration/index.md index e1d7f368d..5e8b63311 100644 --- a/en/02_Developer_Guides/11_Integration/index.md +++ b/en/02_Developer_Guides/11_Integration/index.md @@ -1,7 +1,9 @@ --- +title: Integration and Web Services summary: Integrate other web services within your application or make your Silverstripe CMS data available. introduction: Integrate other web services within your application or make your Silverstripe CMS data available. -title: Integration and Web Services --- +# Integration and web services + [CHILDREN] diff --git a/en/02_Developer_Guides/12_Search/01_Searchcontext.md b/en/02_Developer_Guides/12_Search/01_Searchcontext.md index 9935a5833..062727845 100644 --- a/en/02_Developer_Guides/12_Search/01_Searchcontext.md +++ b/en/02_Developer_Guides/12_Search/01_Searchcontext.md @@ -21,18 +21,18 @@ The default output of a [SearchContext](api:SilverStripe\ORM\Search\SearchContex Defining search-able fields on your DataObject. - ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class MyDataObject extends DataObject +class MyDataObject extends DataObject { - private static $searchable_fields = [ - 'Name', - 'ProductCode' - ]; + private static $searchable_fields = [ + 'Name', + 'ProductCode', + ]; } - ``` ## Customizing fields and filters @@ -41,36 +41,36 @@ In this example we're defining three attributes on our MyDataObject subclass: `P and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDate` should only search for dates *after* the search entry (with a `GreaterThanFilter`). - ```php -use SilverStripe\ORM\Filters\PartialMatchFilter; +namespace App\Model; + +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Filters\GreaterThanFilter; +use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Search\SearchContext; -use SilverStripe\ORM\DataObject; -class MyDataObject extends DataObject +class MyDataObject extends DataObject { - private static $db = [ 'PublicProperty' => 'Text' 'HiddenProperty' => 'Text', - 'MyDate' => 'Date' + 'MyDate' => 'Date', ]; - - public function getDefaultSearchContext() + + public function getDefaultSearchContext() { $fields = $this->scaffoldSearchFields([ - 'restrictFields' => ['PublicProperty','MyDate'] + 'restrictFields' => ['PublicProperty','MyDate'], ]); $filters = [ - 'PublicProperty' => new PartialMatchFilter('PublicProperty'), - 'MyDate' => new GreaterThanFilter('MyDate') + 'PublicProperty' => PartialMatchFilter::create('PublicProperty'), + 'MyDate' => GreaterThanFilter::create('MyDate'), ]; - return new SearchContext( - static::class, - $fields, + return SearchContext::create( + static::class, + $fields, $filters ); } @@ -95,38 +95,42 @@ with advanced options. To customise this field, see the [Scaffolding documentati ### Generating a search form from the context ```php -use SilverStripe\Forms\Form; +namespace App\PageType; + +use App\Model\MyDataObject; +use PageController; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; -use SilverStripe\CMS\Controllers\ContentController; - -// .. -class PageController extends ContentController +class SearchPageController extends PageController { + // ... - public function SearchForm() + public function searchForm() { - $context = singleton('MyDataObject')->getCustomSearchContext(); + $context = MyDataObject::singleton()->getDefaultSearchContext(); $fields = $context->getSearchFields(); - $form = new Form($this, "SearchForm", + $form = Form::create( + $this, + 'searchForm', $fields, - new FieldList( - new FormAction('doSearch') + FieldList::create( + FormAction::create('doSearch') ) ); return $form; } - public function doSearch($data, $form) + public function doSearch($data, $form) { - $context = singleton('MyDataObject')->getCustomSearchContext(); + $context = MyDataObject::singleton()->getDefaultSearchContext(); $results = $context->getResults($data); return $this->customise([ - 'Results' => $results + 'Results' => $results, ])->renderWith('Page_results'); } } @@ -139,55 +143,62 @@ For pagination records on multiple pages, you need to wrap the results in a in order to read page limit information. It is also passed the current `HTTPRequest` object so it can read the current page from a GET var. +Notice that if you want to use this getResults function, you need to change the function doSearch for this one. + +The change is in **$results = $this->getResults($data);**, because you are using a custom getResults function. ```php +namespace App\PageType; + +use App\Model\MyDataObject; +use PageController; use SilverStripe\ORM\PaginatedList; +// ... -public function getResults($searchCriteria = []) +class SearchPageController extends PageController { - $start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0; - $limit = 10; - - $context = singleton('MyDataObject')->getCustomSearchContext(); - $query = $context->getQuery($searchCriteria, null, ['start'=>$start,'limit'=>$limit]); - $records = $context->getResults($searchCriteria, null, ['start'=>$start,'limit'=>$limit]); - - if($records) { - $records = new PaginatedList($records, $this->getRequest()); - $records->setPageStart($start); - $records->setPageLength($limit); - $records->setTotalItems($query->unlimitedRowCount()); + // ... + + public function doSearch($data, $form) + { + $context = MyDataObject::singleton()->getDefaultSearchContext(); + $results = $this->getResults($data); + + return $this->customise([ + 'Results' => $results, + ])->renderWith('Page_results'); } - - return $records; -} -``` -notice that if you want to use this getResults function, you need to change the function doSearch for this one: + public function getResults($searchCriteria = []) + { + $start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0; + $limit = 10; + $context = MyDataObject::singleton()->getDefaultSearchContext(); + $query = $context->getQuery($searchCriteria, null, ['start' => $start,'limit' => $limit]); + $records = $context->getResults($searchCriteria, null, ['start' => $start,'limit' => $limit]); -```php -public function doSearch($data, $form) -{ - $context = singleton('MyDataObject')->getCustomSearchContext(); - $results = $this->getResults($data); - return $this->customise([ - 'Results' => $results - ])->renderWith(['Catalogo_results', 'Page']); + if ($records) { + $records = PaginatedList::create($records, $this->getRequest()); + $records->setPageStart($start); + $records->setPageLength($limit); + $records->setTotalItems($query->unlimitedRowCount()); + } + + return $records; + } } ``` -The change is in **$results = $this->getResults($data);**, because you are using a custom getResults function. - -Another thing you cant forget is to check the name of the singleton you are using in your project. the example uses +Another thing you can't forget is to check the name of the singleton you are using in your project. the example uses **MyDataObject**, you need to change it for the one you are using -### The Pagination Template +### The pagination template to show the results of your custom search you need at least this content in your template, notice that Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like: -**Next 1 2 *3* 4 5 … 558** +**Next 1 2 *3* 4 5 … 558** ```ss <% if $Results %> @@ -206,7 +217,7 @@ Results.PaginationSummary(4) defines how many pages the search will show in the <% if $Results.NotFirstPage %> <% end_if %> - + <% loop $Results.PaginationSummary(4) %> <% if $CurrentBool %> @@ -220,7 +231,7 @@ Results.PaginationSummary(4) defines how many pages the search will show in the <% end_if %> <% end_loop %> - + <% if $Results.NotLastPage %> <% end_if %> @@ -229,15 +240,15 @@ Results.PaginationSummary(4) defines how many pages the search will show in the <% end_if %> ``` -## Available SearchFilters +## Available `SearchFilter` classes See [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) API Documentation -## Related Documentation +## Related documentation -* [ModelAdmin](/developer_guides/customising_the_admin_interface/modeladmin) +- [ModelAdmin](/developer_guides/customising_the_admin_interface/modeladmin) -## API Documentation +## API documentation -* [SearchContext](api:SilverStripe\ORM\Search\SearchContext) -* [DataObject](api:SilverStripe\ORM\DataObject) +- [SearchContext](api:SilverStripe\ORM\Search\SearchContext) +- [DataObject](api:SilverStripe\ORM\DataObject) diff --git a/en/02_Developer_Guides/12_Search/02_FulltextSearch.md b/en/02_Developer_Guides/12_Search/02_FulltextSearch.md index 1bffa33d6..63256995e 100644 --- a/en/02_Developer_Guides/12_Search/02_FulltextSearch.md +++ b/en/02_Developer_Guides/12_Search/02_FulltextSearch.md @@ -16,7 +16,7 @@ a high level wrapper for running advanced search services such as Solr, Lucene o `MySQL` search. [/notice] -## Adding Fulltext Support to MySQLDatabase +## Adding fulltext support to `MySQLDatabase` The [MySQLDatabase](api:SilverStripe\ORM\Connect\MySQLDatabase) class defaults to creating tables using the InnoDB storage engine. As Fulltext search in MySQL requires the MyISAM storage engine, any DataObject you wish to use with Fulltext search must be changed to use MyISAM @@ -24,15 +24,16 @@ storage engine. You can do so by adding this static variable to your class definition: - ```php -use SilverStripe\ORM\DataObject; +namespace App\Model; + use SilverStripe\ORM\Connect\MySQLSchemaManager; +use SilverStripe\ORM\DataObject; -class MyDataObject extends DataObject +class MyDataObject extends DataObject { private static $create_table_options = [ - MySQLSchemaManager::ID => 'ENGINE=MyISAM' + MySQLSchemaManager::ID => 'ENGINE=MyISAM', ]; } ``` @@ -45,51 +46,50 @@ records and cannot easily be adapted to include custom `DataObject` instances. T default site search, have a look at those extensions and modify as required. [/alert] -### Fulltext Filter +### Fulltext filter Silverstripe CMS provides a [FulltextFilter](api:SilverStripe\ORM\Filters\FulltextFilter) which you can use to perform custom fulltext searches on [DataList](api:SilverStripe\ORM\DataList)s. Example DataObject: - ```php -use SilverStripe\ORM\DataObject; +namespace App\Model; + use SilverStripe\ORM\Connect\MySQLSchemaManager; +use SilverStripe\ORM\DataObject; -class SearchableDataObject extends DataObject +class SearchableDataObject extends DataObject { - private static $db = [ - "Title" => "Varchar(255)", - "Content" => "HTMLText", + 'Title' => 'Varchar(255)', + 'Content' => 'HTMLText', ]; private static $indexes = [ 'SearchFields' => [ 'type' => 'fulltext', 'columns' => ['Title', 'Content'], - ] + ], ]; private static $create_table_options = [ - MySQLSchemaManager::ID => 'ENGINE=MyISAM' + MySQLSchemaManager::ID => 'ENGINE=MyISAM', ]; - } - ``` Performing the search: - ```php +use App\Model\SearchableDataOBject; + SearchableDataObject::get()->filter('SearchFields:Fulltext', 'search term'); ``` If your search index is a single field size, then you may also specify the search filter by the name of the field instead of the index. -## API Documentation +## API documentation -* [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) +- [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) diff --git a/en/02_Developer_Guides/12_Search/index.md b/en/02_Developer_Guides/12_Search/index.md index 23f9d184b..476d4362a 100644 --- a/en/02_Developer_Guides/12_Search/index.md +++ b/en/02_Developer_Guides/12_Search/index.md @@ -4,4 +4,6 @@ summary: Provide your users with advanced search functionality. introduction: Give users the ability to search your applications. Fulltext search for Page Content (and other attributes like "Title") can be easily added to Silverstripe CMS. --- +# Search + [CHILDREN] diff --git a/en/02_Developer_Guides/13_i18n/index.md b/en/02_Developer_Guides/13_i18n/index.md index 91870fda7..3a8d8ca0b 100644 --- a/en/02_Developer_Guides/13_i18n/index.md +++ b/en/02_Developer_Guides/13_i18n/index.md @@ -3,20 +3,20 @@ title: i18n summary: Display templates and PHP code in different languages based on the preferences of your website users. --- -# i18n +# I18n The i18n class (short for "internationalization") in Silverstripe CMS enables you to display templates and PHP code in different languages based on your global settings and the preferences of your website users. This process is also known as l10n (short for "localization"). -For translating any content managed through the CMS or stored in the database, we recommend using the +For translating any content managed through the CMS or stored in the database, we recommend using the [Fluent](https://github.com/tractorcow/silverstripe-fluent) module. This page aims to describe the low-level functionality of the i18n API. It targets developers who: -* Are involved in creating templates in different languages. -* Want to build their own modules with i18n capabilities. -* Want to make their PHP-code (e.g. form labels) i18n-ready +- Are involved in creating templates in different languages. +- Want to build their own modules with i18n capabilities. +- Want to make their PHP code (e.g. form labels) i18n-ready ## Usage @@ -26,20 +26,21 @@ The i18n class is enabled by default. ### Setting the locale -To set the locale you just need to call [i18n::set_locale()](api:SilverStripe\i18n\i18n::set_locale()) passing, as a parameter, the name of the locale that +To set the locale you just need to call [i18n::set_locale()](api:SilverStripe\i18n\i18n::set_locale()) passing, as a parameter, the name of the locale that you want to set. - ```php +// app/_config.php use SilverStripe\i18n\i18n; -// app/_config.php -i18n::set_locale('de_DE'); // Setting the locale to German (Germany) -i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra) +// Setting the locale to German (Germany) +i18n::set_locale('de_DE'); +// Setting to Catalan (Andorra) +i18n::set_locale('ca_AD'); ``` Once we set a locale, all the calls to the translator function will return strings according to the set locale value, if -these translations are available. See [unicode.org's Language-Territory Information](https://unicode-org.github.io/cldr-staging/charts/38/supplemental/language_territory_information.html) +these translations are available. See [unicode.org's Language-Territory Information](https://unicode-org.github.io/cldr-staging/charts/38/supplemental/language_territory_information.html) for a complete listing of available locales. The `i18n` logic doesn't set the PHP locale via [`setlocale()`](https://php.net/setlocale). @@ -53,15 +54,17 @@ operations such as decimal separators in database queries. As you set the locale you can also get the current value, just by calling [i18n::get_locale()](api:SilverStripe\i18n\i18n::get_locale()). -### Declaring the content language in HTML {#declaring_the_content_language_in_html} +### Declaring the content language in HTML To let browsers know which language they're displaying a document in, you can declare a language in your template. -```html - +```ss +<%-- 'Page.ss' (HTML) --%> +``` - +```ss +<%-- 'Page.ss' (XHTML) --%> ``` @@ -72,19 +75,16 @@ You can also set the [script direction](https://www.w3.org/International/questio which is determined by the current locale, in order to indicate the preferred flow of characters and default alignment of paragraphs and tables to browsers. - -```html +```ss ``` ### Date and time formats -Formats can be set globally in the i18n class. +Formats can be set globally in the i18n class. You can use these settings for your own view logic. - ```php -use SilverStripe\Core\Config\Config; use SilverStripe\i18n\i18n; i18n::config() @@ -100,12 +100,12 @@ not PHP's built-in [date()](https://www.php.net/manual/en/function.date.php). These settings are not used for CMS presentation. Users can choose their own locale, which determines the date format that gets presented to them. Currently this is a mix of PHP defaults (for readonly `DateField` and `TimeField`), -browser defaults (for `DateField` on browsers supporting HTML5), and [Moment.JS](https://momentjs.com/) +browser defaults (for `DateField` on browsers supporting HTML5), and [Moment.js](https://momentjs.com/) client-side logic (for `DateField` polyfills and other readonly dates and times). ### Adding locales -Silverstripe CMS now uses the php-intl extension. Before adding an extra locale, make sure the ICU library on your server supports it (see https://www.php.net/manual/en/resourcebundle.locales.php for more info). +Silverstripe CMS now uses the php-intl extension. Before adding an extra locale, make sure the ICU library on your server supports it (see for more info). They can be accessed via the `SilverStripe\i18n\Data\Intl\IntlLocales.locales` [config setting](/developer_guides/configuration). @@ -114,11 +114,10 @@ In order to add a value, add the following to your `config.yml`: ```yml SilverStripe\i18n\Data\Intl\IntlLocales: locales: - fr_LU: French (Luxembourg) + fr_LU: French (Luxembourg) ``` - -### i18n in URLs +### I18n in URLs By default, URLs for pages in Silverstripe CMS (the `SiteTree->URLSegment` property) are automatically reduced to the allowed allowed subset of ASCII characters. @@ -129,7 +128,7 @@ are replaced with their base characters, `pâté` becomes `pate`. It is advisable to set the `SS_Transliterator.use_iconv` setting to true via config for systems which have `iconv` extension enabled and configured. -See [the php documentation on iconv](https://www.php.net/manual/en/book.iconv.php) for more information. +See [the PHP documentation on iconv](https://www.php.net/manual/en/book.iconv.php) for more information. In order to allow for so called "multibyte" characters outside of the ASCII subset, limit the character filtering in the underlying configuration setting, @@ -137,7 +136,7 @@ by setting `URLSegmentFilter.default_use_transliterator` to `false` in your YAML Please refer to [W3C: Introduction to IDN and IRI](https://www.w3.org/International/articles/idn-and-iri/) for more details. -### i18n in Form Fields +### I18n in form fields Date and time related form fields are automatically localised ([DateField](api:SilverStripe\Forms\DateField), [TimeField](api:SilverStripe\Forms\TimeField), [DatetimeField](api:SilverStripe\Forms\DatetimeField)). Since they use HTML5 `type=date` and `type=time` fields by default, these fields will present dates @@ -146,13 +145,14 @@ in a localised format chosen by the browser and operating system. Fields can be forced to use a certain locale and date/time format by calling `setHTML5(false)`, followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`. - ```php use SilverStripe\Forms\DateField; $field = new DateField(); -$field->setLocale('de_AT'); // set Austrian/German locale, defaulting format to dd.MM.y -$field->setDateFormat('d.M.y'); // set a more specific date format (single digit day/month) +// set Austrian/German locale, defaulting format to dd.MM.y +$field->setLocale('de_AT'); +// set a more specific date format (single digit day/month) +$field->setDateFormat('d.M.y'); ``` ## Translating text @@ -160,12 +160,11 @@ $field->setDateFormat('d.M.y'); // set a more specific date format (single digit Adapting a module to make it localizable is easy with Silverstripe CMS. You just need to avoid hardcoding strings that are language-dependent and use a translator function call instead. - ```php // without i18n -echo "This is a string"; +echo 'This is a string'; // with i18n -echo _t("Namespace.Entity","This is a string"); +echo _t('Namespace.Entity', 'This is a string'); ``` All strings passed through the `_t()` function will be collected in a separate language table (see [Collecting text](#collecting-text)), which is the starting point for translations. @@ -175,17 +174,16 @@ All strings passed through the `_t()` function will be collected in a separate l The `_t()` function is the main gateway to localized text, and takes four parameters, all but the first being optional. It can be used to translate strings in both PHP files and template files. The usage for each case is described below. -* **$entity:** Unique identifier, composed by a namespace and an entity name, with a dot +- **$entity:** Unique identifier, composed by a namespace and an entity name, with a dot separating them. Both are arbitrary names, although by convention we use the name of the containing class or template. Use this identifier to reference the same translation elsewhere in your code. -* **$default:** The original language string to be translated. This should be declared +- **$default:** The original language string to be translated. This should be declared whenever used, and will get picked up the [text collector](#collecting-text). -* **$string:** (optional) Natural language comment (particularly short phrases and individual words) +- **$string:** (optional) Natural language comment (particularly short phrases and individual words) are very context dependent. This parameter allows the developer to convey this information to the translator. -* **$injection::** (optional) An array of injecting variables into the second parameter - +- **$injection::** (optional) An array of injecting variables into the second parameter ## Pluralisation @@ -203,9 +201,9 @@ with both a 'one' and 'other' key (as per the CLDR for the default `en` language For instance, this is an example of how to correctly declare pluralisations for an object - - ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class MyObject extends DataObject implements i18nEntityProvider @@ -213,9 +211,9 @@ class MyObject extends DataObject implements i18nEntityProvider public function provideI18nEntities() { return [ - 'MyObject.SINGULARNAME' => 'object', - 'MyObject.PLURALNAME' => 'objects', - 'MyObject.PLURALS' => [ + __CLASS__ . '.SINGULARNAME' => 'object', + __CLASS__ . '.PLURALNAME' => 'objects', + __CLASS__ . '.PLURALS' => [ 'one' => 'An object', 'other' => '{count} objects', ], @@ -227,9 +225,9 @@ class MyObject extends DataObject implements i18nEntityProvider In YML format this will be expressed as the below. This follows the [ruby i18n convention](https://guides.rubyonrails.org/i18n.html#pluralization) for plural forms. -```yaml +```yml en: - MyObject: + App\Model\MyObject: SINGULARNAME: 'object' PLURALNAME: 'objects' PLURALS: @@ -240,16 +238,16 @@ en: Note: i18nTextCollector support for pluralisation is not yet available. Please ensure that any required plurals are exposed via provideI18nEntities. -#### Usage in PHP Files - +### Usage in PHP files ```php // Simple string translation -_t('SilverStripe\\Admin\\LeftAndMain.FILESIMAGES','Files & Images'); +_t('SilverStripe\\Admin\\LeftAndMain.FILESIMAGES', 'Files & Images'); // Using injection to add variables into the translated strings. -_t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED', - "Restored {value} successfully", +_t( + 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED', + 'Restored {value} successfully', ['value' => $itemRestored] ); @@ -261,15 +259,14 @@ _t(self::class . '.GREETING', 'Welcome!'); _t(__CLASS__ . '.GREETING', 'Welcome!'); ``` -#### Usage in Template Files +### Usage in template files In `.ss` template files, instead of `_t(params)` the syntax `<%t params %>` is used. The syntax for passing parameters to the function is quite different to the PHP version of the function. - * Parameters are space separated, not comma separated - * The original language string and the natural language comment parameters are separated by ` on `. - * The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs. - +- Parameters are space separated, not comma separated +- The original language string and the natural language comment parameters are separated by ` on `. +- The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs. ```ss <%-- Simple string translation --%> @@ -282,9 +279,9 @@ the PHP version of the function. <%t MyObject.PLURALS 'An item|{count} items' count=$Count %> ``` -#### Caching in Template Files with locale switching +### Caching in template files with locale switching -When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key +When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key otherwise it won't pick up locale changes. ```ss @@ -319,24 +316,24 @@ with prefix `themes:`, e.g: [hint] You can also run this task via the command line using sake, e.g: -```sh +```bash sake dev/tasks/i18nTextCollectorTask module=themes:my-theme,silverstripe/framework ``` See [the sake documentation](/developer_guides/cli/) for details about using sake. [/hint] -## Module Priority +## Module priority The order in which i18n strings are loaded from modules can be quite important, as it is pretty common for a site developer to want to override the default i18n strings from time to time. Because of this, you will sometimes need to specify the loading priority of i18n modules. By default, the language files are loaded from modules in this order: - * Your project (as defined in the `$project` global) - * admin - * framework - * All other modules +- Your project (as defined in the `$project` global) +- admin +- framework +- All other modules This default order is configured in `framework/_config/i18n.yml`. This file specifies two blocks of module ordering: `basei18n`, listing admin, and framework, and `defaulti18n` listing all other modules. @@ -353,12 +350,13 @@ SilverStripe\i18n\i18n: - module2 - module3 ``` + The config option being set is `i18n.module_priority`, and it is a list of module names. There are a few special cases: - * If not explicitly mentioned, your project is put as the first module. - * The module name `other_modules` can be used as a placeholder for all modules that aren't +- If not explicitly mentioned, your project is put as the first module. +- The module name `other_modules` can be used as a placeholder for all modules that aren't specifically mentioned. ## Language definitions @@ -389,7 +387,7 @@ de: Note that translations are cached across requests. The cache can be cleared through the `?flush=1` query parameter. -## Javascript Usage +## JavaScript usage The i18n system in JavaScript is similar to its PHP equivalent. Languages are typically stored in `/javascript/lang`. @@ -404,7 +402,7 @@ The `Requirements` class has a special method to determine these includes: Just point it to a directory instead of a file, and the class will figure out the includes. If using this on the frontend, it's also necessary to include the stand-alone i18n -js file. +JS file. ```php use SilverStripe\View\Requirements; @@ -414,48 +412,47 @@ Requirements::add_i18n_javascript('/javascript/lang'); ``` You can also include the language files from the public resources folder with the resource syntax: + ```php Requirements::add_i18n_javascript('vendor/module:path/to/lang'); ``` -### Translation Tables in JavaScript +### Translation tables in JavaScript Translation tables are automatically included as required, depending on the configured locale in `i18n::get_locale()`. As a fallback for partially translated tables we always include the dist table (`en.js`) as well. Dist Table (`/javascript/lang/en.js`) - ```js -if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') { +if (typeof (ss) === 'undefined' || typeof (ss.i18n) === 'undefined') { + /* eslint-disable-next-line no-console */ console.error('Class ss.i18n not defined'); } else { ss.i18n.addDictionary('en', { - 'MYMODULE.MYENTITY' : "Really delete these articles?" + 'MYMODULE.MYENTITY': 'Really delete these articles?' }); } ``` Example Translation Table (`/javascript/lang/de.js`) - ```js ss.i18n.addDictionary('de', { - 'MYMODULE.MYENTITY' : "Artikel wirklich löschen?" + 'MYMODULE.MYENTITY': 'Artikel wirklich löschen?' }); ``` For most core modules, these files are generated by a build task, with the actual source files in a JSON format which can be processed more easily by external translation providers (see `javascript/lang/src`). -### Basic Usage - +### Basic usage ```js -alert(ss.i18n._t('MYMODULE.MYENTITY')); +const myText = ss.i18n._t('MYMODULE.MYENTITY'); ``` -### Advanced Use +### Advanced use The `ss.i18n` object contain a couple functions to help and replace dynamic variable from within a string. @@ -467,12 +464,12 @@ is done sequentially. ```js // MYMODULE.MYENTITY contains "Really delete %s articles by %s?" -alert(ss.i18n.sprintf( - ss.i18n._t('MYMODULE.MYENTITY'), - 42, - 'Douglas Adams' -)); -// Displays: "Really delete 42 articles by Douglas Adams?" +// The myText variable contains: "Really delete 42 articles by Douglas Adams?" +const myText = ss.i18n.sprintf( + ss.i18n._t('MYMODULE.MYENTITY'), + 42, + 'Douglas Adams' +); ``` #### Variable injection with inject() @@ -481,24 +478,23 @@ alert(ss.i18n.sprintf( keys in the object passed as second argument. Each variable can be in any order and appear multiple times. - ```js // MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?" -alert(ss.i18n.inject( - ss.i18n._t('MYMODULE.MYENTITY'), - {count: 42, author: 'Douglas Adams'} -)); -// Displays: "Really delete 42 articles by Douglas Adams?" +// The myText variable contains: "Really delete 42 articles by Douglas Adams?" +const myText = ss.i18n.inject( + ss.i18n._t('MYMODULE.MYENTITY'), + { count: 42, author: 'Douglas Adams' } +); ``` ## Limitations -* No detecting/conversion of character encodings (we rely fully on UTF-8) -* Translation of graphics/assets -* Usage of gettext (too clumsy, too many requirements) -* Displaying multiple languages/encodings on the same page +- No detecting/conversion of character encodings (we rely fully on UTF-8) +- Translation of graphics/assets +- Usage of gettext (too clumsy, too many requirements) +- Displaying multiple languages/encodings on the same page ## Links - * [Help to translate](../../contributing/translations) - Instructions for online collaboration to translate core - * [Help to translate](../../contributing/translation_process) - Instructions for adding translation to your own modules +- [Help to translate](../../contributing/translations) - Instructions for online collaboration to translate core +- [Help to translate](../../contributing/translation_process) - Instructions for adding translation to your own modules diff --git a/en/02_Developer_Guides/14_Files/01_File_Management.md b/en/02_Developer_Guides/14_Files/01_File_Management.md index 8c15facdb..e04483988 100644 --- a/en/02_Developer_Guides/14_Files/01_File_Management.md +++ b/en/02_Developer_Guides/14_Files/01_File_Management.md @@ -14,23 +14,23 @@ control over the publishing and security of files. ![asset admin](../../_images/asset-admin-demo.png) -## UploadField +## `UploadField` If you have the [silverstripe/asset-admin](https://github.com/silverstripe/silverstripe-asset-admin) -module installed then this provides a powerful component [api:SilverStripe\AssetAdmin\Forms\UploadField]. +module installed then this provides a powerful component [`UploadField`](api:SilverStripe\AssetAdmin\Forms\UploadField). ![upload field](../../_images/upload-field.png) You can add it to a page as below: ```php - Image::class, @@ -47,24 +47,24 @@ class Page extends SiteTree UploadField options include: - - setIsMultiUpload() - Set to allow many files per field, or one only. - - setAllowedExtensions() - Set list of extensions this field can accept. - - setAllowedFileCategories() - Alternatively specify allowed extensions via category instead. - - setFolderName() - Name of folder to upload into - - getValidator() - Get instance of validator to specify custom validation rules +- setIsMultiUpload() - Set to allow many files per field, or one only. +- setAllowedExtensions() - Set list of extensions this field can accept. +- setAllowedFileCategories() - Alternatively specify allowed extensions via category instead. +- setFolderName() - Name of folder to upload into +- getValidator() - Get instance of validator to specify custom validation rules ## File permissions {#permissions} -See [File Security](file_security). +See [File Security](file_security). ## File visibility In order to ensure that assets are made public you should check the following: - - The "Who can view this file?" option is set to "Anyone" or "Inherit" in the asset-admin. This can be checked +- The "Who can view this file?" option is set to "Anyone" or "Inherit" in the asset-admin. This can be checked via `File::canView()` or `File::$CanViewType` property. - - The file is published, or is owned by a published record. This can be checked via `File::isPublished()` - - The file exists on disk, and has not been removed. This can be checked by `File::exists()` +- The file is published, or is owned by a published record. This can be checked via `File::isPublished()` +- The file exists on disk, and has not been removed. This can be checked by `File::exists()` ## File shortcodes @@ -78,9 +78,9 @@ of a page with a shortcode image: File shortcodes have the following properties: - - canView() will not be checked for the file itself: Instead this will be inherited from the parent record +- canView() will not be checked for the file itself: Instead this will be inherited from the parent record this is embedded within. - - The file is automatically "owned", meaning that publishing the page will also publish the embedded file. +- The file is automatically "owned", meaning that publishing the page will also publish the embedded file. Within the CMS shortcodes can be added via either the "Insert Media" modal, or the "Link to a file" buttons provided via the [silverstripe/asset-admin](https://github.com/silverstripe/silverstripe-asset-admin) @@ -88,17 +88,15 @@ module. ## Creating files in PHP -When working with files in PHP you can upload a file into a [api:SilverStripe\Assets\File] dataobject +When working with files in PHP you can upload a file into a [`File`](api:SilverStripe\Assets\File) dataobject using one of the below methods: - | Method | Description | | -------------------------- | --------------------------------------- | | `File::setFromLocalFile` | Load a local file into the asset store | | `File::setFromStream` | Will store content from a stream | | `File::setFromString` | Will store content from a binary string | - ### Upload conflict resolution When storing files, it's possible to determine the mechanism the backend should use when it encounters @@ -123,7 +121,7 @@ As with storage, there are also different ways of loading the content (or proper | ------------------------ | ---------------------------------------------------------- | | `File::getStream` | Will get an output stream of the file content | | `File::getString` | Gets the binary content | -| `File::getURL` | Gets the url for this resource. May or may not be absolute | +| `File::getURL` | Gets the URL for this resource. May or may not be absolute | | `File::getAbsoluteURL` | Gets the absolute URL to this resource | | `File::getMimeType` | Get the mime type of this file | | `File::getMetaData` | Gets other metadata from the file as an array | @@ -157,8 +155,8 @@ $file = File::get()->filter('Name', 'oldname.jpg')->first(); if ($file) { // The below will move 'oldname.jpg' and 'oldname__variant.jpg' // to 'newname.jpg' and 'newname__variant.jpg' respectively - $file->Name = 'newname.jpg'; - $file->write(); + $file->Name = 'newname.jpg'; + $file->write(); } ``` @@ -172,41 +170,42 @@ $file = File::get()->filter('Name', 'oldname.jpg')->first(); if ($file) { // The below will immediately move 'oldname.jpg' and 'oldname__variant.jpg' // to 'newname.jpg' and 'newname__variant.jpg' respectively - $file->Name = 'newname.jpg'; - Versioned::withVersionedMode(function() use ($file) { - Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); - $file->write(); - $file->publishSingle(); - }); + $file->Name = 'newname.jpg'; + Versioned::withVersionedMode(function () use ($file) { + Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + $file->write(); + $file->publishSingle(); + }); } ``` ## Adding custom fields to files and images -As with any customisation of a core class, adding fields to the `File` and `Image` classes +As with any customisation of a core class, adding fields to the `File` and `Image` classes is a two-phased approach. First, you have to update the model (i.e. the `$db` array) to include your new custom field. Second, you need to update the editform to provide a way of editing that field in the CMS. For most core classes, this can be done in a single extension, with an -update to the `$db` array and definition of an `updateCMSFields` function, but for files +update to the `$db` array and definition of an `updateCMSFields` function, but for files and images, it works a bit differently. The edit form is generated by another class -- `FileFormFactory`. You will therefore need two separate extensions. In this example, we'll add a `description` field to the `File` object and give it an editable field in the CMS. -*app/_config/app.yml* ```yml +# app/_config/extensions.yml SilverStripe\Assets\File: extensions: - - MyProject\MyFileExtension + - App\Extension\MyFileExtension + SilverStripe\AssetAdmin\Forms\FileFormFactory: extensions: - - MyProject\MyFormFactoryExtension + - App\Extension\MyFormFactoryExtension ``` -*app/src/MyFileExtension.php* ```php -namespace MyProject; +// app/src/Extension/MyFileExtension.php +namespace App\Extension; use SilverStripe\ORM\DataExtension; @@ -218,9 +217,9 @@ class MyFileExtension extends DataExtension } ``` -*app/src/MyFormFactoryExtension.php* ```php -namespace MyProject; +// app/src/Extension/MyFormFactoryExtension.php +namespace App\Extension; use SilverStripe\Core\Extension; use SilverStripe\Forms\FieldList; @@ -238,14 +237,13 @@ class MyFormFactoryExtension extends Extension } ``` - ## File versioning File versioning is extended with the [silverstripe/versioned](https://github.com/silverstripe/silverstripe-versioned/) module, which provides not only a separate draft and live stages for any file, but also allows a complete file history of modifications to be tracked. -To support this feature the [api:SilverStripe\Assets\AssetControlExtension] provides support for tracking +To support this feature the [`AssetControlExtension`](api:SilverStripe\Assets\AssetControlExtension) provides support for tracking references to physical files, ensuring published assets are accessible, protecting non-published assets, and archiving / deleting assets after the final reference has been deleted. @@ -258,12 +256,12 @@ is published, all assets that are used by this record are published with it. For example: ```php - Image::class, @@ -277,11 +275,10 @@ See [Versioned: Ownership](/developer_guides/model/versioning#ownership) for det ### Avoid exclusive relationships Due to the shared nature of assets, it is not recommended to assign any one-to-many (or exclusive one-to-one) relationship -between any objects and a File. E.g. a Page has_many File, or Page belongs_to File. - +between any objects and a File. E.g. a Page `has_many` File, or Page `belongs_to` File. Instead it is recommended to use either a Page has_one File for many-to-one (or one-to-one) relationships, or -Page many_many File for many-to-many relationships. +Page `many_many` File for many-to-many relationships. ### Unpublishing assets @@ -298,10 +295,11 @@ In order to permanently keep a record of all past physical files you can set the config option to true. This will ensure that historic files can always be restored, albeit at a cost to disk storage. -```yaml +```yml SilverStripe\Assets\File: keep_archived_assets: true ``` -## Related Lessons -* [Working with files and images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) +## Related lessons + +- [Working with files and images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) diff --git a/en/02_Developer_Guides/14_Files/02_Images.md b/en/02_Developer_Guides/14_Files/02_Images.md index ba2d240f6..a9fd3b695 100644 --- a/en/02_Developer_Guides/14_Files/02_Images.md +++ b/en/02_Developer_Guides/14_Files/02_Images.md @@ -25,7 +25,7 @@ into an `` tag on your website automatically. See [HTMLEditorField](/forms/field-types/htmleditorfield). -### Manipulating images in Templates +### Manipulating images in templates You can manipulate images directly from templates to create images that are resized and cropped to suit your needs. This doesn't affect the original @@ -33,11 +33,10 @@ image or clutter the CMS with any additional files, and any images you create in this way are cached for later use. In most cases the pixel aspect ratios of images are preserved (meaning images are not stretched). -![](../../_images/image-methods.jpg) +![a series of images with different manipulations applied](../../_images/image-methods.jpg) Here are some examples, assuming the `$Image` object has dimensions of 200x100px: - ```ss // Scaling functions $Image.ScaleWidth(150) // Returns a 150x75px image @@ -77,7 +76,7 @@ Image methods are chainable. Example: ``` -### Padded Image Resize +### Padded image resize The Pad method allows you to resize an image with existing ratio and will pad any surplus space. You can specify the color of the padding using a hex code such as FFFFFF or 000000. @@ -85,11 +84,13 @@ pad any surplus space. You can specify the color of the padding using a hex code You can also specify a level of transparency to apply to the padding color in a fourth param. This will only effect png images. - ```php -$Image.Pad(80, 80, FFFFFF, 50) // white padding with 50% transparency -$Image.Pad(80, 80, FFFFFF, 100) // white padding with 100% transparency -$Image.Pad(80, 80, FFFFFF) // white padding with no transparency +// white padding with 50% transparency +$Image . Pad(80, 80, FFFFFF, 50) +// white padding with 100% transparency +$Image . Pad(80, 80, FFFFFF, 100) +// white padding with no transparency +$Image . Pad(80, 80, FFFFFF) ``` ### Manipulating images in PHP @@ -104,15 +105,18 @@ Please refer to the [`ImageManipulation`](api:SilverStripe\Assets\ImageManipulat You can also create your own functions by decorating the `Image` class. - ```php +namespace App\Extension; + +use SilverStripe\Assets\Image_Backend; use SilverStripe\Core\Extension; + class ImageExtension extends Extension { - public function Square($width) + public function getSquare($width) { $variant = $this->owner->variantName(__FUNCTION__, $width); - return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use($width) { + return $this->owner->manipulateImage($variant, function (Image_Backend $backend) use ($width) { $clone = clone $backend; $resource = clone $backend->getImageResource(); $resource->fit($width); @@ -121,10 +125,10 @@ class ImageExtension extends Extension }); } - public function Blur($amount = null) + public function getBlur($amount = null) { $variant = $this->owner->variantName(__FUNCTION__, $amount); - return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use ($amount) { + return $this->owner->manipulateImage($variant, function (Image_Backend $backend) use ($amount) { $clone = clone $backend; $resource = clone $backend->getImageResource(); $resource->blur($amount); @@ -132,24 +136,25 @@ class ImageExtension extends Extension return $clone; }); } - } ``` ```yml SilverStripe\Assets\Image: extensions: - - ImageExtension + - App\Extension\ImageExtension SilverStripe\Assets\Storage\DBFile: extensions: - - ImageExtension + - App\Extension\ImageExtension ``` -### Form Upload +These can then be used directly in your templates, e.g. `$MyImage.Square` and `$MyImage.Blur`. + +### Form upload For usage on a website form, see [`FileField`](api:SilverStripe\Assets\FileField). -### Image Quality +### Image quality #### Source images @@ -157,12 +162,12 @@ Whenever Silverstripe CMS performs a manipulation on an image, it saves the outp as a new image file, and applies compression during the process. If the source image already had lossy compression applied, this leads to the image being compressed twice over which can produce a poor result. To ensure the best -quality output images, it's recommended to upload high quality source images +quality output images, it's recommended to upload high quality source images (minimal or no compression) in to your asset store, and let Silverstripe CMS take care of applying compression. -Very high resolution images may cause GD to crash (especially on shared hosting -environments where resources are limited) so a good size for website images is +Very high resolution images may cause GD to crash (especially on shared hosting +environments where resources are limited) so a good size for website images is around 2000px on the longest edge. #### Forced resampling @@ -192,7 +197,6 @@ SilverStripe\Assets\Storage\DBFile: To adjust the quality of the generated images when they are resampled, add the following to your `app/_config/config.yml` file: - ```yml SilverStripe\Core\Injector\Injector: SilverStripe\Assets\Image_Backend: @@ -200,10 +204,10 @@ SilverStripe\Core\Injector\Injector: Quality: 90 ``` -### Lazy Loading {#lazy-loading} +### Lazy loading {#lazy-loading} -Most modern browsers support the ability to "lazy load" images by adding a `loading="lazy"` attribute -to the `` tag. This defers the loading of images not in the viewport to improve the initial +Most modern browsers support the ability to "lazy load" images by adding a `loading="lazy"` attribute +to the `` tag. This defers the loading of images not in the viewport to improve the initial page load performance. Silverstripe CMS automatically adds the `loading="lazy"` to images added in an HTML editor field @@ -213,17 +217,17 @@ Content authors have the ability to selectively disable lazy loading when insert HTML editor field. Read [Browser-level image lazy-loading for the web](https://web.dev/browser-level-image-lazy-loading/) -on _web.dev_ for more information. +on *web.dev* for more information. #### Selectively disabling lazy loading in SS templates -Images that are expected to be initially visible on page load, should be _eager_ loaded. This -provides a small performance gain since the browser doesn't have to render the entire page layout -before determining if the images need to be loaded. When in doubt, it's usually preferable to lazy +Images that are expected to be initially visible on page load, should be *eager* loaded. This +provides a small performance gain since the browser doesn't have to render the entire page layout +before determining if the images need to be loaded. When in doubt, it's usually preferable to lazy load the image. -Developers can selectively disable lazy loading for individual image in a SS template by calling -`LazyLoad(false)` on the image variable (e.g.: `$MyImage.LazyLoad(false)`). +Developers can selectively disable lazy loading for individual image in a SS template by calling +`LazyLoad(false)` on the image variable (e.g: `$MyImage.LazyLoad(false)`). ```ss <%-- Image will be lazy loaded --%> @@ -236,45 +240,45 @@ $Logo.LazyLoad(false) $Logo.LazyLoad($LogoLoading) ``` -Developers can allow content authors to control the loading attribute of a specific image by +Developers can allow content authors to control the loading attribute of a specific image by adding a lazy load field next to the [`UploadField`](api:SilverStripe\Assets\UploadField). ```php - 'Boolean' + 'LogoLoading' => 'Boolean', ]; private static $has_one = [ - 'Logo' => Image::class + 'Logo' => Image::class, ]; private static $defaults = [ - 'LogoLoading' => true + 'LogoLoading' => true, ]; - public function getCMSFields() { $fields = parent::getCMSFields(); $loadingSource = [ true => 'Lazy (Default)', - false => 'Eager' + false => 'Eager', ]; - + $fields->addFieldsToTab( 'Root.Main', [ UploadField::create('Logo'), - DropdownField::create('LogoLoading', 'Loading', $loadingSource) + DropdownField::create('LogoLoading', 'Loading', $loadingSource), ] ); @@ -304,15 +308,15 @@ page after the initial page load. #### Disabling lazy loading globally -To opt out of lazy loading globally, notably if you already have a custom lazy loading -implementation, use the following yml config: +To opt out of lazy loading globally, notably if you already have a custom lazy loading +implementation, use the following YAML config: ```yml SilverStripe\Assets\Image: lazy_loading_enabled: false ``` -## Changing the manipulation driver to Imagick +## Changing the manipulation driver to imagick If you want to change the image manipulation driver to use Imagick instead of GD, you'll need to change your config so that the `Intervention\Image\ImageManager` is instantiated with the `imagick` driver instead of GD: @@ -328,13 +332,13 @@ SilverStripe\Core\Injector\Injector: Manipulated images are stored as "file variants" in the same folder structure as the original image. The storage mechanism is described in the ["File Storage" guide](file_storage). -## API Documentation +## API documentation + +- [File](api:SilverStripe\Assets\File) +- [Image](api:SilverStripe\Assets\Image) +- [DBFile](api:SilverStripe\Assets\Storage\DBFile) +- [ImageManipulation](api:SilverStripe\Assets\ImageManipulation) - * [File](api:SilverStripe\Assets\File) - * [Image](api:SilverStripe\Assets\Image) - * [DBFile](api:SilverStripe\Assets\Storage\DBFile) - * [ImageManipulation](api:SilverStripe\Assets\ImageManipulation) +## Related lessons -## Related Lessons -* [Working with files and images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) - +- [Working with files and images](https://www.silverstripe.org/learn/lessons/v4/working-with-files-and-images-1) diff --git a/en/02_Developer_Guides/14_Files/03_File_Security.md b/en/02_Developer_Guides/14_Files/03_File_Security.md index ff0973591..065f77d18 100644 --- a/en/02_Developer_Guides/14_Files/03_File_Security.md +++ b/en/02_Developer_Guides/14_Files/03_File_Security.md @@ -4,7 +4,7 @@ summary: Manage access permission to assets icon: lock --- -# File Security +# File security ## Overview @@ -19,51 +19,52 @@ There's two dimensions in which to classify how a file can be accessed. Versioning stage: - * "Draft file" (default): A file which hasn't been published (default after upload). +- "Draft file" (default): A file which hasn't been published (default after upload). A subset of "protected file". See [versioning](/developer_guides/model/versioning). - * "Published file": A published file (can be protected by further access restrictions). +- "Published file": A published file (can be protected by further access restrictions). Files are often published indirectly as part of the objects who own them (see [File Ownership](file_management#ownership)). Access restrictions: - * "Unprotected file" (default): A file without access restrictions. - * "Protected file": A file with access restrictions. +- "Unprotected file" (default): A file without access restrictions. +- "Protected file": A file with access restrictions. Note that draft files are always protected, and even published files can be protected if they have access restrictions. -## Permission Model +## Permission model Like all other objects in Silverstripe CMS, permissions are generally controlled via `can*()` methods, for example `canView()` (see [permissions](/developer_guides/security/permissions)). The permission model defines the following actions: - * View: Access file metadata in the database. - * Edit: Edit file metadata as well as replacing the file content. - * Create: Create file metadata and upload file content. - * Delete: Delete file metadata and the file content. - * Download: Access the file content, but not the file metadata. +- View: Access file metadata in the database. +- Edit: Edit file metadata as well as replacing the file content. +- Create: Create file metadata and upload file content. +- Delete: Delete file metadata and the file content. +- Download: Access the file content, but not the file metadata. Usually treated the same as "View". There's a few rules guiding their access, in descending order of priority: - * Published and unprotected files can be downloaded by anyone knowing the URL. +- Published and unprotected files can be downloaded by anyone knowing the URL. They bypass any Silverstripe CMS permission checks (served directly by the webserver). - * Access can be restricted by custom `can*()` method implementations on `File` +- Access can be restricted by custom `can*()` method implementations on `File` (through [extensions](/developer_guides/extending/extensions)). This logic can overrule any further restrictions below. - * Users with "Full administrative rights" (`ADMIN` permission code) +- Users with "Full administrative rights" (`ADMIN` permission code) have view and edit access by default, regardless of further restrictions below. - * Users with "Edit any file" permissions (`FILE_EDIT_ALL` permission code) +- Users with "Edit any file" permissions (`FILE_EDIT_ALL` permission code) have edit access by default, regardless of further restrictions below. - * View or edit access can be restricted per file or folder through - an inherited permissions model similar to page content (through [api:SilverStripe\Security\InheritedPermissionsExtension]). +- View or edit access can be restricted per file or folder through + an inherited permissions model similar to page content (through [`InheritedPermissionsExtension`](api:SilverStripe\Security\InheritedPermissionsExtension)). There are five types: "Inherit from parent" (default), "Anyone", "Logged-in users", "Only these groups", or "Only these users". - * Protected files (incl. draft files) allow view/edit access when `File::$non_live_permissions` is satisfied. +- Protected files (incl. draft files) allow view/edit access when `File::$non_live_permissions` is satisfied. By default, that's configured for anyone with access to any CMS section, or the ability to "view draft content". - * Protected files need an "access grant" for the current session + +- Protected files need an "access grant" for the current session in order to download the file (see [User access control](#user-access-control)). While you can technically allow viewing or editing a file without granting access to download it, those aspects are usually bundled together by the file viewing logic. @@ -81,7 +82,7 @@ When implementing your own `canView()` logic through [extensions](/developer_gui existing unprotected files are not retroactively moved to the protected asset store. While those new permissions are honoured in the CMS, protected files through custom `canView()` can still be downloaded through a public URL until a `write()` operation is triggered on them. -[/warning] +[/warning] ## Asset stores @@ -93,11 +94,10 @@ instructed to favour private or protected stores in some cases. For instance, in order to write an asset to a protected location you can use the following additional config option: - ```php $store = singleton(AssetStore::class); $store->setFromString('My protected content', 'my-folder/my-file.jpg', null, null, [ - 'visibility' => AssetStore::VISIBILITY_PROTECTED + 'visibility' => AssetStore::VISIBILITY_PROTECTED, ]); ``` @@ -112,7 +112,6 @@ An automated system will, in most cases, handle this whitelisting for you. Calls will automatically whitelist access to that file for the current user. Using this as a guide, you can easily control access to embedded assets at a template level. - ```ss
            <% loop $File %> @@ -136,18 +135,20 @@ partial cache block) will not whitelist those files automatically. You can manua file via PHP for the current user instead, by using the following code to grant access. ```php -use SilverStripe\CMS\Controllers\ContentController; +namespace { + use SilverStripe\CMS\Controllers\ContentController; -class PageController extends ContentController -{ - public function init() + class PageController extends ContentController { - parent::init(); - - // Whitelist the protected files on this page for the current user - $file = $this->File(); - if($file->canView()) { - $file->grantFile(); + public function init() + { + parent::init(); + + // Whitelist the protected files on this page for the current user + $file = $this->File(); + if ($file->canView()) { + $file->grantFile(); + } } } } @@ -157,33 +158,36 @@ If a user does not have access to a file, you can still generate the URL but sup permission whitelist by invoking the getter as a method, but pass in a falsey value as a parameter. (or '0' in template as a workaround for all parameters being cast as string) - ```ss <% if not $canView %> - + <%-- The user will be denied if they follow this url --%>
          • Access to $Title is denied
          • <% else %> + ... +<% end_if %> ``` Alternatively, if a user has already been granted access, you can explicitly revoke their access using the `revokeFile` method. ```php -use SilverStripe\CMS\Controllers\ContentController; +namespace { + use SilverStripe\CMS\Controllers\ContentController; -class PageController extends ContentController -{ - public function init() + class PageController extends ContentController { - parent::init(); - - // Whitelist the protected files on this page for the current user - $file = $this->File(); - if($file->canView()) { - $file->grantFile(); - } else { - // Will revoke any historical grants - $file->revokeFile(); + public function init() + { + parent::init(); + + // Whitelist the protected files on this page for the current user + $file = $this->File(); + if ($file->canView()) { + $file->grantFile(); + } else { + // Will revoke any historical grants + $file->revokeFile(); + } } } } @@ -200,11 +204,11 @@ or `AssetStore::VISIBILITY_PUBLIC` constants. It's advisable to ensure the visib is declared as early as possible, so that potentially sensitive content never touches any public facing area. -E.g. +For example: ```php $object->MyFile->setFromLocalFile($tmpFile['Path'], $filename, null, null, [ - 'visibility' => AssetStore::VISIBILITY_PROTECTED + 'visibility' => AssetStore::VISIBILITY_PROTECTED, ]); ``` @@ -240,8 +244,7 @@ in the protected store, awaiting publishing. Internally your folder structure would look something like: - -``` +```text assets/ OldCompanyLogo.gif .protected/ @@ -252,10 +255,10 @@ assets/ The urls for these two files, however, do not reflect the physical structure directly. -* The public file at `https://www.example.com/assets/OldCompanyLogo.gif` will be served directly from the web server, +- The public file at `https://www.example.com/assets/OldCompanyLogo.gif` will be served directly from the web server, and will not invoke a PHP request. -* The protected file at `https://www.example.com/assets/a870de278b/NewCompanyLogo.gif` will be routed via a 404 handler to PHP, - which will be passed to the `[ProtectedFileController](api:SilverStripe\Assets\Storage\ProtectedFileController)` controller, which will serve +- The protected file at `https://www.example.com/assets/a870de278b/NewCompanyLogo.gif` will be routed via a 404 handler to PHP, + which will be passed to the [`ProtectedFileController`](api:SilverStripe\Assets\Storage\ProtectedFileController) controller, which will serve up the content of the hidden file, conditional on a permission check. When the file `NewCompanyLogo.gif` is made public, the file @@ -271,7 +274,7 @@ $store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1' After this the filesystem will now look like below: -``` +```text assets/ NewCompanyLogo.gif .protected/ @@ -289,7 +292,7 @@ directly may lead to necessary security checks being omitted. See the web server setting section below for more information on configuring your server properly -### Performance: Static caching +### Performance: static caching If you are deploying your site to a server configuration that makes use of static caching, it's essential that you ensure any page or dataobject cached adequately publishes any linked assets. This is due to the @@ -309,7 +312,7 @@ require developers to adjust the HTTP response for file requests. Most of the routing logic for serving Files is controlled via the `AssetStore` interface. The default implementation of the `AssetStore` is `FlysystemAssetStore`. -### Configuring: Protected folder location +### Configuring: protected folder location In the default Silverstripe CMS configuration, protected assets are placed within the web root into the `assets/.protected` folder, into which is also generated a `.htaccess` or `web.config` configured @@ -321,11 +324,11 @@ root altogether. For instance, given your web root is in the folder `/sites/myapp/www`, you can tell the asset store to put protected files into `/sites/myapp/protected` with the below `.env` setting: -``` +```bash SS_PROTECTED_ASSETS_PATH="/sites/myapp/protected" ``` -### Configuring: Protected file headers {#protected_file_headers} +### Configuring: protected file headers {#protected_file_headers} In certain situations, it's necessary to customise HTTP headers required either by intermediary caching services, or by the client, or upstream caches. @@ -334,7 +337,7 @@ When a protected file is served it will also be transmitted with all headers def `SilverStripe\Filesystem\Flysystem\FlysystemAssetStore.file_response_headers` config. You can customise this with the below config: -```yaml +```yml SilverStripe\Filesystem\Flysystem\FlysystemAssetStore: file_response_headers: Pragma: 'no-cache' @@ -365,9 +368,7 @@ before it's sent to the client by applying an `Extension` to `FlysystemAssetStor To achieve this create an `Extension` and implement the `updateResponse` method. ```php - php_admin_flag engine off @@ -526,7 +531,7 @@ will need to make sure you manually configure these rules. For instance, this will allow your nginx site to serve files directly, while ensuring dynamic requests are processed via the Framework: -``` +```text location ^~ /assets/ { sendfile on; try_files $uri index.php?$query_string; diff --git a/en/02_Developer_Guides/14_Files/04_File_Storage.md b/en/02_Developer_Guides/14_Files/04_File_Storage.md index 4b7fae62f..e2376ca60 100644 --- a/en/02_Developer_Guides/14_Files/04_File_Storage.md +++ b/en/02_Developer_Guides/14_Files/04_File_Storage.md @@ -7,49 +7,47 @@ icon: hdd # File storage This section describes how the asset store abstraction layer stores the physical files underlying the ORM, -and explains some of the considerations. +and explains some of the considerations. -## Component Overview +## Component overview The assets module is composed of these major storage classes: -* [api:SilverStripe\Assets\File]: This is the main DataObject that user code interacts with when working with files. +- [`File`](api:SilverStripe\Assets\File): This is the main DataObject that user code interacts with when working with files. This class has the following subclasses: - - [api:SilverStripe\Assets\Folder]: Logical folder which holds a set of files. These can be nested. - - [api:SilverStripe\Assets\Image]: Specialisation of File representing an image which can be resized. + - [`Folder`](api:SilverStripe\Assets\Folder): Logical folder which holds a set of files. These can be nested. + - [`Image`](api:SilverStripe\Assets\Image): Specialisation of File representing an image which can be resized. Note that this does not include non-resizable image files. -* [api:SilverStripe\Assets\Storage\DBFile]: This is the DB field used by the File dataobject internally for +- [`DBFile`](api:SilverStripe\Assets\Storage\DBFile): This is the DB field used by the File dataobject internally for storing references to physical files in the asset backend. -* [api:SilverStripe\Assets\Flysystem\FlysystemAssetStore]: The default backend, provided by +- [`FlysystemAssetStore`](api:SilverStripe\Assets\Flysystem\FlysystemAssetStore): The default backend, provided by [Flysystem](https://flysystem.thephpleague.com/docs/), which Silverstripe CMS uses as an asset persistence layer. -* [api:SilverStripe\Assets\InterventionBackend]: Default image resizing mechanism, provided by +- [`InterventionBackend`](api:SilverStripe\Assets\InterventionBackend): Default image resizing mechanism, provided by [intervention image](https://image.intervention.io/). These interfaces are also provided to abstract certain behaviour: -* [api:SilverStripe\Assets\Storage\AssetContainer]: Abstract interface for a file reference. Implemented by both +- [`AssetContainer`](api:SilverStripe\Assets\Storage\AssetContainer): Abstract interface for a file reference. Implemented by both File and DBFile. Declare API for reading to and writing an single file. -* [api:SilverStripe\Assets\Storage\AssetStore]: Abstract interface for the backend store for the asset system. +- [`AssetStore`](api:SilverStripe\Assets\Storage\AssetStore): Abstract interface for the backend store for the asset system. Implemented by FlysystemAssetStore. Declares API for reading and writing assets from and to the store. ## Storage via database columns -Asset storage is provided out of the box via a [Flysystem](https://flysystem.thephpleague.com/docs/) backed store. +Asset storage is provided out of the box via a [Flysystem](https://flysystem.thephpleague.com/docs/) backend store. However, any class that implements the `AssetStore` interface could be substituted to provide storage backends via other mechanisms. -Internally, files are stored as [DBFile](api:SilverStripe\Assets\Storage\DBFile) records on the rows of parent objects. +Internally, files are stored as [`DBFile`](api:SilverStripe\Assets\Storage\DBFile) records on the rows of parent objects. These records are composite fields which contain sufficient information useful to the configured asset backend in order to store, manage, and publish files. By default this composite field behind this field stores the following details: - | Field name | Description | -| ---------- | ----------- +| ---------- | ----------- | `Hash` | The sha1 of the file content, useful for versioning (if supported by the backend) | | `Filename` | The internal identifier for this file, which may contain a directory path (not including assets). Multiple versions of the same file will have the same filename. | | `Variant` | The variant for this file. If a file has multiple derived versions (such as resized files or reformatted documents) then you can point to one of the variants here. | - Note that the `Hash` and `Filename` always point to the original file, if a `Variant` is specified. It is up to the storage backend to determine how variants are managed. @@ -62,7 +60,7 @@ Public files are published either directly through the "Assets" CMS UI, or indirectly as part of a [versioned ownership structure](/developer_guides/model/versioning). They are stored as you'd expect on the filesystem: In their folder, by their file name. -``` +```text assets/ my-public-folder/ my-public-file.jpg @@ -71,7 +69,7 @@ assets/ The URL for this file will match the physical location on disk: `https://www.example.com/assets/my-public-folder/my-public-file.jpg`. -## Variant file paths (e.g. resized images) {#variant-file-paths} +## Variant file paths (e.G. Resized images) {#variant-file-paths} Each file can have variants, most commonly resized versions of an image. These can be generated by resizing an image in the CMS rich text editor, @@ -79,7 +77,7 @@ through template logic, or programmatically with PHP. They are stored in the same folder alongside the original file, but contain a special variant suffix. -``` +```text assets/ my-public-folder/ my-public-file.jpg @@ -96,7 +94,7 @@ that requires permissions to view them. Protected files can also be published but access restricted. In either case, they're stored in a special `assets/.protected` folder. In this case, they're stored in a folder matching the truncated hash of the file's content. -``` +```text assets/ my-public-folder/ my-public-file.jpg @@ -127,7 +125,7 @@ see [Server Requirements: Secure Assets](/getting_started/server_requirements#se Older versions of file contents are kept in the `.protected` folder, following the same rules as [protected file paths](#protected-file-paths). -``` +```text assets/ my-file.jpg <- current published file .protected/ @@ -141,7 +139,7 @@ assets/ By default, when files are replaced or removed, their original file contents aren't retained in order to avoid bloat on the filesystem. -Changes are only tracked for file metadata (e.g. the `Title` attribute). +Changes are only tracked for file metadata (e.g. the `Title` attribute). You can opt-in to retaining the file content for replaced or removed files. @@ -152,7 +150,7 @@ SilverStripe\Assets\File: The filesystem structure follows the same rules as [protected file paths](#protected-file-paths): -``` +```text assets/ my-file.jpg <- current published file .protected/ @@ -160,7 +158,7 @@ assets/ my-file.jpg b63923d8d4/ <- old content hash of replaced file version my-file.jpg -``` +``` ## Loading content into `DBFile` @@ -170,16 +168,22 @@ within the assets folder). For example, to load a temporary file into a DataObject you could use the below: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Banner extends DataObject +class Banner extends DataObject { private static $db = [ - 'Image' => 'DBFile' + 'Image' => 'DBFile', ]; } +``` + +```php +use App\Model\Banner; // Image could be assigned in other parts of the code using the below -$banner = new Banner(); +$banner = Banner::create(); $banner->Image->setFromLocalFile($tempfile['path'], 'my-folder/my-file.jpg'); ``` diff --git a/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md b/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md index 2a581cd8f..214455488 100644 --- a/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md +++ b/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md @@ -9,8 +9,9 @@ icon: lock Not every kind of file should be stored in a CMS's asset management system. For example, allowing users to upload JavaScript files could lead to a risk of Cross-Site Scripting (XSS) attacks. Out of the box, your Silverstripe CMS project will limit what type of files can be uploaded into the assets management section. There's two type of restriction in place based on: -* the extensions of the files -* the MIME type of the files. + +- the extensions of the files +- the MIME type of the files. ## File extensions validation @@ -18,7 +19,8 @@ The `silverstripe/assets` module ships with a whitelist of allowed file extensio The whitelist is controlled by the `SilverStripe\Assets\File::$allowed_extensions` variable. -You can whitelist additional file extensions by adding them in your YML configuration. +You can whitelist additional file extensions by adding them in your YAML configuration. + ```yml SilverStripe\Assets\File: allowed_extensions: @@ -56,7 +58,7 @@ Look at the `app/_config/mimevalidator.yml` file to view the default configurati You can explicitly require the module by running this command -```sh +```bash composer require silverstripe/mimevalidator ``` @@ -93,9 +95,9 @@ SilverStripe\MimeValidator\MimeUploadValidator: ## Adding new image types {#add-image-format} -Silverstripe CMS support JPEG, GIF, PNG and WebP image formats out of the box. Silverstripe CMS can be configured to support other less common image formats (e.g.: AVIF). For this to work, your version of PHP and of the [`intervention/image` library](https://intervention.io/) must support these alternative image formats. +Silverstripe CMS support JPEG, GIF, PNG and WebP image formats out of the box. Silverstripe CMS can be configured to support other less common image formats (e.g: AVIF). For this to work, your version of PHP and of the [`intervention/image` library](https://intervention.io/) must support these alternative image formats. -For example, this snippet can be added to the configuration of older Silverstripe CMS projects to allow them to work with WebP images. +For example, this snippet can be added to the configuration of older Silverstripe CMS projects to allow them to work with WebP images. ```yml --- diff --git a/en/02_Developer_Guides/14_Files/07_File_Usage.md b/en/02_Developer_Guides/14_Files/07_File_Usage.md index c9480ad40..8051a4a8b 100644 --- a/en/02_Developer_Guides/14_Files/07_File_Usage.md +++ b/en/02_Developer_Guides/14_Files/07_File_Usage.md @@ -4,35 +4,34 @@ summary: See file usage and customising the file "Used on" table icon: compress-arrows-alt --- -# File Usage +# File usage CMS users can view where a file is used by accessing the Used On tab in the Files section. This feature allows them to identify what DataObjects depend on the file. In the Files section of the CMS, click on a file to see file details. Within the file details panel there is a Used on tab that shows a table of Pages and other DataObjects where the file is used throughout the website. -## Customising the File "Used on" table in the Files section (asset-admin) +## Customising the file "Used on" table in the files section (asset-admin) Your project specific DataObject will automatically be displayed on the Used on tab. This may not always be desirable, especially when working with background DataObjects the user can not interact with directly. Extensions can be applied to the `UsedOnTable` class to update specific entries. Extension hooks can be used to do the following: + - Exclude DataObjects of a particular type of class from being fetched from the database - Exclude individual DataObjects that were fetched for showing on the used on table - Link ancestors of a DataObject so they show on the same row of the used on table -### Example PHP file: +### Example PHP file ```php - 'Varchar', 'ProductCode' => 'Varchar', - 'Price' => 'Currency' + 'Price' => 'Currency', ]; private static $has_one = [ - 'Category' => Category::class + 'Category' => Category::class, ]; } ``` -**app/src/Category.php** - - ```php +// app/src/Model/Category.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Category extends DataObject +class Category extends DataObject { - private static $db = [ - 'Title' => 'Text' + 'Title' => 'Text', ]; private static $has_many = [ - 'Products' => Product::class + 'Products' => Product::class, ]; } ``` @@ -64,18 +62,19 @@ DataObject's you want to scaffold an interface for. The class can manage multipl We'll name it `MyAdmin`, but the class name can be anything you want. -**app/src/MyAdmin.php** - - ```php +// app/src/Admin/MyAdmin.php +namespace App\Admin; + +use App\Model\Category; +use App\Model\Product; use SilverStripe\Admin\ModelAdmin; -class MyAdmin extends ModelAdmin +class MyAdmin extends ModelAdmin { - private static $managed_models = [ Product::class, - Category::class + Category::class, ]; private static $url_segment = 'products'; @@ -91,46 +90,49 @@ users will be able to upload and manage `Product` and `Category` instances throu After defining these classes, make sure you have rebuilt your Silverstripe CMS database and flushed your cache. [/alert] -## Defining the ModelAdmin models +## Defining the `ModelAdmin` models The `$managed_models` configuration supports additional formats allowing you to customise the URL and tab label used to access a specific model. This can also be used to display the same model more than once with different filtering or display options. ```php +namespace App\Admin; + +use App\Model\Category; +use App\Model\Product; use SilverStripe\Admin\ModelAdmin; -class MyAdmin extends ModelAdmin +class MyAdmin extends ModelAdmin { - private static $managed_models = [ // This is the most basic format. URL for this Model will use the fully // qualified namespace of `Product`. The label for this tab will be determined // by the `i18n_plural_name` on the `Product` class. Product::class, - + // This format can be used to customise the tab title. Category::class => [ - 'title' => 'All categories' + 'title' => 'All categories', ], - + // This format can be used to customise the URL segment for this Model. This can // be useful if you do not want the fully qualified class name of the Model to // appear in the URL. It can also be used to have the same Model appear more than // once, allowing you to create custom views. 'product-category' => [ 'dataClass' => Category::class, - 'title' => 'Product categories' - ] + 'title' => 'Product categories', + ], ]; private static $url_segment = 'products'; private static $menu_title = 'My Product Admin'; - + public function getList() { - $list = parent::getList(); + $list = parent::getList(); // Only show Categories specific to Products When viewing the product-category tab if ($this->modelTab === 'product-category') { $list = $list->filter('IsProductCategory', true); @@ -138,7 +140,6 @@ class MyAdmin extends ModelAdmin return $list; } } - ``` ### Edit links for records @@ -186,51 +187,58 @@ The [DataObject](api:SilverStripe\ORM\DataObject) API has more granular permissi Available checks are `canEdit()`, `canCreate()`, `canView()` and `canDelete()`. Models check for administrator permissions by default. For most cases, less restrictive checks make sense, e.g. checking for general CMS access rights. -**app/src/Category.php** - - ```php -use SilverStripe\Security\Permission; +// app/src/Model/Category.php +namespace App\Model; + use SilverStripe\ORM\DataObject; +use SilverStripe\Security\Permission; -class Category extends DataObject +class Category extends DataObject { - public function canView($member = null) + // ... + + public function canView($member = null) { return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member); } - public function canEdit($member = null) + public function canEdit($member = null) { return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member); } - public function canDelete($member = null) + public function canDelete($member = null) { return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member); } - public function canCreate($member = null) + public function canCreate($member = null) { return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member); } } ``` -## Custom ModelAdmin CSS menu icons using built in icon font +## Custom `ModelAdmin` CSS menu icons using built in icon font An extended ModelAdmin class supports adding a custom menu icon to the CMS. -``` +```php +namespace App\Admin; + +use SilverStripe\Admin\ModelAdmin; + class NewsAdmin extends ModelAdmin { - ... private static $menu_icon_class = 'font-icon-news'; + // ... } ``` + A complete list of supported font icons is available to view in the [Silverstripe CMS Design System Manager](https://projects.invisionapp.com/dsm/silver-stripe/silver-stripe/section/icons/5a8b972d656c91001150f8b6) -## Searching Records +## Searching records [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) uses the [SearchContext](../search/searchcontext) class to provide a search form, as well as get the searched results. Every [DataObject](api:SilverStripe\ORM\DataObject) can have its own context, based on the fields which should be searchable. The @@ -240,19 +248,20 @@ class makes a guess at how those fields should be searched, e.g. showing a check To remove, add or modify searchable fields, define a new [DataObject::$searchable_fields](api:SilverStripe\ORM\DataObject::$searchable_fields) static on your model class (see [Searchable Fields](/developer_guides/model/scaffolding#searchable-fields) and [SearchContext](../search/searchcontext) docs for details). -**app/src/Product.php** - - ```php +// app/src/Model/Product.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Product extends DataObject +class Product extends DataObject { + // ... - private static $searchable_fields = [ + private static $searchable_fields = [ 'Name', - 'ProductCode' - ]; + 'ProductCode', + ]; } ``` @@ -260,29 +269,32 @@ class Product extends DataObject [SearchContext](../search/searchcontext) documentation has more information on providing the search functionality. [/hint] -## Displaying Results +## Displaying results The results are shown in a tabular listing, powered by the [GridField](../forms/field_types/gridfield), more specifically the [GridFieldDataColumns](api:SilverStripe\Forms\GridField\GridFieldDataColumns) component. This component looks for a [DataObject::$summary_fields](api:SilverStripe\ORM\DataObject::$summary_fields) static on your model class, where you can add or remove columns. To change the title, use [DataObject::$field_labels](api:SilverStripe\ORM\DataObject::$field_labels). See [Summary Fields](/developer_guides/model/scaffolding#summary-fields) and [Field labels](/developer_guides/model/scaffolding#field-labels) for details. -**app/src/Product.php** - - ```php +// app/src/Model/Product.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Product extends DataObject +class Product extends DataObject { - private static $field_labels = [ - 'Price' => 'Cost' // renames the column to "Cost" - ]; + // ... - private static $summary_fields = [ + private static $field_labels = [ + // renames the column to "Cost" + 'Price' => 'Cost', + ]; + + private static $summary_fields = [ 'Name', - 'Price' - ]; + 'Price', + ]; } ``` @@ -292,21 +304,24 @@ can be customized by additional SQL filters, joins. For example, we might want to exclude all products without prices in our sample `MyAdmin` implementation. -**app/src/MyAdmin.php** - - ```php -modelClass == 'Product') { + if ($this->modelClass == Product::class) { $list = $list->exclude('Price', '0'); } @@ -327,33 +342,37 @@ class MyAdmin extends ModelAdmin You can also customize the search behavior directly on your `ModelAdmin` instance. For example, we might want to have a checkbox which limits search results to expensive products (over $100). -**app/src/MyAdmin.php** - ```php -modelClass == 'Product') { + if ($this->modelClass == Product::class) { $context->getFields()->push(CheckboxField::create('q[ExpensiveOnly]', 'Only expensive stuff')); } return $context; } - public function getList() + public function getList() { $list = parent::getList(); - $params = $this->getRequest()->requestVar('q'); // use this to access search parameters + // use this to access search parameters + $params = $this->getRequest()->requestVar('q'); - if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) { + if ($this->modelClass == Product::class && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) { $list = $list->exclude('Price:LessThan', '100'); } @@ -362,37 +381,38 @@ class MyAdmin extends ModelAdmin } ``` -## Altering the ModelAdmin GridField or Form +## Altering the `ModelAdmin`, `GridField`, or `Form` If you wish to provided a tailored esperience for CMS users, you can directly interact with the ModelAdmin form or gridfield. Override the following method: -* `getEditForm()` to alter the Form object -* `getGridField()` to alter the GridField field -* `getGridFieldConfig()` to alter the GridField configuration. + +- `getEditForm()` to alter the Form object +- `getGridField()` to alter the GridField field +- `getGridFieldConfig()` to alter the GridField configuration. Extensions applied to a ModelAdmin can also use the `updateGridField` and `updateGridFieldConfig` hooks. -To alter how the results are displayed (via [GridField](api:SilverStripe\Forms\GridField\GridField)), you can also override the `getEditForm()` method. For +To alter how the results are displayed (via [`GridField`](api:SilverStripe\Forms\GridField\GridField)), you can also override the `getEditForm()` method. For example, to add a new component. -### Overriding the methods on ModelAdmin - -**app/src/MyAdmin.php** - +### Overriding the methods on `ModelAdmin` ```php - 'Name', 'ProductCode' => 'Product Code', - 'Category.Title' => 'Category' + 'Category.Title' => 'Category', ]; } } ``` -## Related Lessons -* [Intoduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) +## Related lessons + +- [Intoduction to ModelAdmin](https://www.silverstripe.org/learn/lessons/v4/introduction-to-modeladmin-1) -## Related Documentation +## Related documentation -* [GridField](../forms/field_types/gridfield) -* [Permissions](../security/permissions) -* [SearchContext](../search/searchcontext) +- [GridField](../forms/field_types/gridfield) +- [Permissions](../security/permissions) +- [SearchContext](../search/searchcontext) -## API Documentation +## API documentation -* [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) -* [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) -* [GridField](api:SilverStripe\Forms\GridField\GridField) -* [DataList](api:SilverStripe\ORM\DataList) -* [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) +- [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) +- [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) +- [GridField](api:SilverStripe\Forms\GridField\GridField) +- [DataList](api:SilverStripe\ORM\DataList) +- [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md index bf08dfb5a..fa4f05631 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md @@ -3,7 +3,8 @@ title: CMS Architecture summary: An overview of the code architecture of the CMS icon: sitemap --- -# CMS Architecture + +# CMS architecture ## Introduction @@ -30,11 +31,11 @@ The pattern library can be used to preview React components without including th The easiest way to access the pattern library is to view it online. The pattern library for the latest Silverstripe CMS development branch is automatically built and deployed. Note that this may include new components that are not yet available in a stable release. -[Browse the Silverstripe CMS pattern library online](https://silverstripe.github.io/silverstripe-pattern-lib/). +[Browse the Silverstripe CMS pattern library online](https://silverstripe.g.ithub.io/silverstripe-pattern-lib/). ### Running the pattern library -If you're developing a new React component, running the pattern library locally is a good way to interact with it. +If you're developing a new React component, running the pattern library locally is a good way to interact with it. The pattern library is built from the `silverstripe/admin` module, but it also requires `silverstripe/asset-admin`, `silversrtipe/cms` and `silverstripe/campaign-admin`. @@ -52,15 +53,13 @@ The pattern library will be available at `http://localhost:6006`. The JS source If you want to build a static version of the pattern library, you can replace `yarn pattern-lib` with `yarn build-storybook`. This will output the pattern library files to a `storybook-static` folder. -The Silverstripe CMS pattern library is built using the [StoryBook JS library](https://storybook.js.org/). You can read the StoryBook documentation to learn about more advanced features and customisation options. +The Silverstripe CMS pattern library is built using the [StoryBook JS library](https://storybook.js.org/). You can read the StoryBook documentation to learn about more advanced features and customisation options. -## The Admin URL +## The admin URL The CMS interface can be accessed by default through the `admin/` URL. You can change this by setting the `$url_base` config for the [AdminRootController](api:SilverStripe\Admin\AdminRootController), creating your own [Director](api:SilverStripe\Control\Director) routing rule and clearing the old rule as per the example below: - ```yml - --- Name: myadmin After: @@ -70,32 +69,34 @@ SilverStripe\Control\Director: rules: 'admin': '' 'newAdmin': 'SilverStripe\Admin\AdminRootController' - + SilverStripe\Admin\AdminRootController: - url_base: 'newAdmin' + url_base: 'newAdmin' --- ``` When extending the CMS or creating modules, you can take advantage of various functions that will return the configured admin URL (by default 'admin' is returned): [warning] -Depending on your configuration, the returned value _may or may not_ include a trailing slash. The default is to not include one, but you should take care to not +Depending on your configuration, the returned value *may or may not* include a trailing slash. The default is to not include one, but you should take care to not explicitly expect one scenario or the other. In PHP you can use [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) or pass an argument to [AdminRootController::admin_url()](api:SilverStripe\Admin\AdminRootController::admin_url()) to ensure only one `/` character separates the admin URL from the rest of your path. -In javascript, if you are using [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config), you can use the `joinUrlPaths()` utility +In JavaScript, if you are using [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config), you can use the `joinUrlPaths()` utility function. [/warning] In PHP you should use: ```php -SilverStripe\Admin\AdminRootController::admin_url() +use SilverStripe\Admin\AdminRootController; + +AdminRootController::admin_url() // This method can take an argument: -SilverStripe\Admin\AdminRootController::admin_url('more/path/here') +AdminRootController::admin_url('more/path/here') ``` When writing templates use: @@ -106,21 +107,20 @@ $AdminURL $AdminURL('more/path/here') ``` -And in JavaScript, this is available through the `ss` namespace +And in JavaScript, this is available through the `ss` namespace as `ss.config.adminUrl` ```js -ss.config.adminUrl - // You can use this if you use @silverstripe/webpack-config import { joinUrlPaths } from 'lib/urls'; -joinUrlPaths(ss.config.adminUrl, 'more/path/here') + +joinUrlPaths(ss.config.adminUrl, 'more/path/here'); ``` -### Multiple Admin URL and overrides +### Multiple admin URL and overrides You can also create your own classes that extend the [AdminRootController](api:SilverStripe\Admin\AdminRootController) to create multiple or custom admin areas, with a `Director.rules` for each one. -## Templates and Controllers +## Templates and controllers The CMS backend is handled through the [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) controller class, which contains base functionality like displaying and saving a record. @@ -154,7 +154,7 @@ These placeholders are populated by auto-detected templates, with the naming convention of `_Tools.ss` and `_EditFormTools.ss`. So to add or "subclass" a tools panel, simply create this file and it's automatically picked up. -## Layout and Panels +## Layout and panels The various panels and UI components within them are loosely coupled to the layout engine through the `data-layout-type` attribute. The layout is triggered on the top element and cascades into children, with a `redraw` method defined on @@ -166,13 +166,13 @@ Refer to [Layout reference](/developer_guides/customising_the_admin_interface/cm Silverstripe CMS constructs forms and its fields within PHP, mainly through the [getCMSFields()](api:SilverStripe\ORM\DataObject::getCMSFields()) method. -This in turn means that the CMS loads these forms as HTML via Ajax calls, -e.g. after saving a record (which requires a form refresh), or switching the section in the CMS. +This in turn means that the CMS loads these forms as HTML via Ajax calls, e.g. +after saving a record (which requires a form refresh), or switching the section in the CMS. Depending on where in the DOM hierarchy you want to use a form, custom templates and additional CSS classes might be required for correct operation. For example, the "EditForm" has specific view and logic JavaScript behaviour -which can be enabled via adding the "cms-edit-form" class. +which can be enabled via adding the "CMS-edit-form" class. In order to set the correct layout classes, we also need a custom template. To obey the inheritance chain, we use `$this->getTemplatesWithSuffix('_EditForm')` for selecting the most specific template (so `MyAdmin_EditForm.ss`, if it exists). @@ -182,31 +182,34 @@ of a `PjaxResponseNegotiator` to handle its display. Basic example form in a CMS controller subclass: - ```php -use SilverStripe\Forms\TabSet; +namespace App\Admin; + +use SilverStripe\Admin\LeftAndMain; +use SilverStripe\Admin\LeftAndMainFormRequestHandler; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\FormAction; use SilverStripe\Forms\Tab; +use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TextField; -use SilverStripe\Forms\FormAction; -use SilverStripe\Admin\LeftAndMain; -use SilverStripe\Admin\LeftAndMainFormRequestHandler; -class MyAdmin extends LeftAndMain +class MyAdmin extends LeftAndMain { - public function getEditForm() { + public function getEditForm() + { return Form::create( $this, 'EditForm', - new FieldList( + FieldList::create( TabSet::create( 'Root', - Tab::create('Main', + Tab::create( + 'Main', TextField::create('MyText') ) )->setTemplate('CMSTabset') ), - new FieldList( + FieldList::create( FormAction::create('doSubmit') ) ) @@ -229,7 +232,7 @@ Note: Usually you don't need to worry about these settings, and will simply call `parent::getEditForm()` to modify an existing, correctly configured form. -## JavaScript through jQuery.entwine +## JavaScript through jQuery.Entwine [notice] The following documentation regarding Entwine does not apply to React components or sections powered by React. @@ -250,7 +253,7 @@ in jQuery.entwine, we're trying to reuse library code wherever possible. The most prominent example of this is the usage of [jQuery UI](https://jqueryui.com) for dialogs and buttons. -## JavaScript and CSS dependencies via Requirements and Ajax +## JavaScript and CSS dependencies via requirements and ajax The JavaScript logic powering the CMS is divided into many files, which typically are included via the [Requirements](api:SilverStripe\View\Requirements) class, by adding @@ -281,13 +284,14 @@ so as a developer just declare your dependencies through the [Requirements](api: Silverstripe CMS uses the HTML5 browser history to modify the URL without a complete window refresh. We us the below systems in combination to achieve this: - * [Page.js](https://github.com/visionmedia/page.js) routing library is used for most - cms sections, which provides additional Silverstripe CMS specific functionality via the + +- [Page.js](https://github.com/visionmedia/page.js) routing library is used for most + CMS sections, which provides additional Silverstripe CMS specific functionality via the `vendor/silverstripe/admin/client/src/lib/Router.js` wrapper. - The router is available on `window.ss.router` and provides the same API as - described in the - [Page.js docs](https://github.com/visionmedia/page.js#api). - * [React router](https://github.com/reactjs/react-router) is used for react-powered + The router is available on `window.ss.router` and provides the same API as + described in the + [Page.js docs](https://github.com/visionmedia/page.js#api). +- [React router](https://github.com/reactjs/react-router) is used for react-powered CMS sections. This provides a native react-controlled bootstrapping and route handling system that works most effectively with react components. Unlike page.js routes, these may be lazy-loaded or registered during the lifetime of the application on the @@ -295,14 +299,14 @@ window refresh. We us the below systems in combination to achieve this: ### Registering routes -#### page.js (non-react) CMS sections +#### `Page.js` (non-react) CMS sections CMS sections that rely on entwine, page.js, and normal ajax powered content loading mechanisms (such as modeladmin) will typically have a single wildcard route that initiates the [pjax loading mechanism](#pjax). The main place that routes are registered are via the `LeftAndMain::getClientConfig()` overridden method, -which by default registers a single 'url' route. This will generate a wildcard route handler for each CMS +which by default registers a single 'URL' route. This will generate a wildcard route handler for each CMS section in the form `/admin/
            (/*)?`, which will capture any requests for this section. Additional routes can be registered like so `window.ss.router('admin/pages', callback)`, however @@ -331,11 +335,20 @@ controlled by the react section, and thus should suppress registration of a page for this section. ```php -public function getClientConfig() +namespace App\Admin; + +use SilverStripe\Admin\LeftAndMain; + +class MyAdmin extends LeftAndMain { - return array_merge(parent::getClientConfig(), [ - 'reactRouter' => true, - ]); + // ... + + public function getClientConfig() + { + return array_merge(parent::getClientConfig(), [ + 'reactRouter' => true, + ]); + } } ``` @@ -349,15 +362,15 @@ import reactRouteRegister from 'lib/ReactRouteRegister'; import MyAdmin from './MyAdmin'; document.addEventListener('DOMContentLoaded', () => { - const sectionConfig = ConfigHelpers.getSection('MyAdmin'); - - reactRouteRegister.add({ - path: sectionConfig.reactRoutePath, - component: MyAdminComponent, - childRoutes: [ - { path: 'form/:id/:view', component: MyAdminComponent }, - ], - }); + const sectionConfig = ConfigHelpers.getSection('MyAdmin'); + + reactRouteRegister.add({ + path: sectionConfig.reactRoutePath, + component: MyAdminComponent, + childRoutes: [ + { path: 'form/:id/:view', component: MyAdminComponent }, + ], + }); }); ``` @@ -367,12 +380,12 @@ Child routes can be registered post-boot by using `ReactRouteRegister` in the sa // Register a nested url under `sectionConfig.reactRoutePath` const sectionConfig = ConfigHelpers.getSection('MyAdmin'); reactRouteRegister.add({ - path: 'nested', - component: NestedComponent, -}, [ sectionConfig.reactRoutePath ]); + path: 'nested', + component: NestedComponent, +}, [sectionConfig.reactRoutePath]); ``` -## PJAX: Partial template replacement through Ajax {#pjax} +## PJAX: partial template replacement through ajax {#pjax} Many user interactions can change more than one area in the CMS. For example, editing a page title in the CMS form changes it in the page tree @@ -395,33 +408,37 @@ Example: Create a bare-bones CMS subclass which shows breadcrumbs (a built-in me as well as info on the current record. A single link updates both sections independently in a single Ajax request. - ```php +// app/src/Admin/MyAdmin.php +namespace App\Admin; + use SilverStripe\Admin\LeftAndMain; -// app/src/MyAdmin.php -class MyAdmin extends LeftAndMain +class MyAdmin extends LeftAndMain { private static $url_segment = 'myadmin'; - public function getResponseNegotiator() + + public function getResponseNegotiator() { $negotiator = parent::getResponseNegotiator(); $controller = $this; // Register a new callback - $negotiator->setCallback('MyRecordInfo', function() use(&$controller) { - return $controller->MyRecordInfo(); + $negotiator->setCallback('MyRecordInfo', function () use (&$controller) { + return $controller->getMyRecordInfo(); }); + return $negotiator; } - public function MyRecordInfo() + + public function getMyRecordInfo() { return $this->renderWith('MyRecordInfo'); } } ``` -**MyAdmin.ss** ```ss +<%-- MyAdmin.ss --%> <% include SilverStripe\\Admin\\CMSBreadcrumbs %>
            Static content (not affected by update)
            <% include MyRecordInfo %> @@ -430,8 +447,8 @@ class MyAdmin extends LeftAndMain ``` -**MyRecordInfo.ss** ```ss +<%-- MyRecordInfo.ss --%>
            Current Record: $currentPage.Title
            @@ -439,7 +456,7 @@ class MyAdmin extends LeftAndMain A click on the link will cause the following (abbreviated) ajax HTTP request: -```HTTP +```text GET /admin/myadmin HTTP/1.1 X-Pjax:MyRecordInfo,Breadcrumbs X-Requested-With:XMLHttpRequest @@ -447,7 +464,7 @@ X-Requested-With:XMLHttpRequest ... and result in the following response: -```JSON +```json {"MyRecordInfo": " { + // Say 'success'! + // eslint-disable-next-line no-alert + alert(status); + }); ``` -## Ajax Redirects +## Ajax redirects Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form, the resulting view will be the edit form of the new record. On non-ajax submissions, that's easily @@ -552,16 +571,19 @@ For example, the currently used controller class might've changed due to a "redi which affects the currently active menu entry. We're using HTTP response headers to contain this data without affecting the response body. - ```php +namespace App\Control; + use SilverStripe\Admin\LeftAndMain; -class MyController extends LeftAndMain +class MyController extends LeftAndMain { - class myaction() + // ... + + public function myaction() { // ... - $this->getResponse()->addHeader('X-Controller', 'MyOtherController'); + $this->getResponse()->addHeader('X-Controller', MyOtherController::class); return $html; } } @@ -569,20 +591,20 @@ class MyController extends LeftAndMain Built-in headers are: - * `X-Title`: Set window title (requires URL encoding) - * `X-Controller`: PHP class name matching a menu entry, which is marked active - * `X-ControllerURL`: Alternative URL to record in the HTML5 browser history - * `X-Status`: Extended status information, used for an information popover (aka "toast" message). - * `X-Reload`: Force a full page reload based on `X-ControllerURL` +- `X-Title`: Set window title (requires URL encoding) +- `X-Controller`: PHP class name matching a menu entry, which is marked active +- `X-ControllerURL`: Alternative URL to record in the HTML5 browser history +- `X-Status`: Extended status information, used for an information popover (aka "toast" message). +- `X-Reload`: Force a full page reload based on `X-ControllerURL` -## Special Links +## Special links Some links should do more than load a new page in the browser window. To avoid repetition, we've written some helpers for various use cases: - * Load into a PJAX panel: `` - * Load URL as an iframe into a popup/dialog: `` - * GridField click to redirect to external link: `` +- Load into a PJAX panel: `` +- Load URL as an iframe into a popup/dialog: `` +- GridField click to redirect to external link: `` ## Buttons @@ -616,13 +638,21 @@ from "Page" to "Files & Images". To communicate this state change, a controller response has the option to pass along a special HTTP response header, which is picked up by the menu: - ```php -public function mycontrollermethod() +namespace App\Control; + +use SilverStripe\Control\Controller; + +class MyController extends Controller { - // .. logic here - $this->getResponse()->addHeader('X-Controller', 'AssetAdmin'); - return 'my response'; + // ... + + public function mycontrollermethod() + { + // ... logic here + $this->getResponse()->addHeader('X-Controller', 'AssetAdmin'); + return 'my response'; + } } ``` @@ -669,7 +699,6 @@ since all others should render with their tab navigation inline. Form template with custom tab navigation (trimmed down): - ```ss
            @@ -698,7 +727,6 @@ Form template with custom tab navigation (trimmed down): Tabset template without tab navigation (e.g. `CMSTabset.ss`) - ```ss
            <% loop Tabs %> @@ -723,7 +751,6 @@ This is achieved by template conditionals (see "MyActiveCondition"). The `.cms-panel-link` class will automatically trigger the ajax loading, and load the HTML content into the main view. Example: - ```ss
              @@ -745,11 +772,10 @@ The URL endpoints `$AdminURL('mytabs/tab1')` and `$AdminURL('mytabs/tab2')` should return HTML fragments suitable for inserting into the content area, through the `PjaxResponseNegotiator` class (see above). - ## Related - * [Howto: Extend the CMS Interface](/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface) - * [Howto: Customise the CMS tree](/developer_guides/customising_the_admin_interface/how_tos/customise_cms_tree) - * [ModelAdmin API](api:SilverStripe\Admin\ModelAdmin) - * [Reference: Layout](/developer_guides/customising_the_admin_interface/cms_layout) - * [Rich Text Editing](/developer_guides/forms/field_types/htmleditorfield) +- [Howto: Extend the CMS Interface](/developer_guides/customising_the_admin_interface/how_tos/extend_cms_interface) +- [Howto: Customise the CMS tree](/developer_guides/customising_the_admin_interface/how_tos/customise_cms_tree) +- [ModelAdmin API](api:SilverStripe\Admin\ModelAdmin) +- [Reference: Layout](/developer_guides/customising_the_admin_interface/cms_layout) +- [Rich Text Editing](/developer_guides/forms/field_types/htmleditorfield) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md index 462b39315..6677c0c32 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md @@ -29,11 +29,11 @@ $('.cms-container').redraw(); This causes the framework to: -* reset the _threeColumnCompressor_ algorithm with the current layout options (that can be set via +- reset the *threeColumnCompressor* algorithm with the current layout options (that can be set via `updateLayoutOptions`) -* trigger `layout` which cascades into all children resizing and positioning subordinate elements (this is internal +- trigger `layout` which cascades into all children resizing and positioning subordinate elements (this is internal to the layout manager) -* trigger `redraw` on children which also cascades deeper into the hierarchy (this is framework activity) +- trigger `redraw` on children which also cascades deeper into the hierarchy (this is framework activity) [notice] Caveat: `layout` is also triggered when a DOM element is replaced with AJAX in `LeftAndMain::handleAjaxResponse`. In @@ -51,25 +51,24 @@ avoid incorrect dimensions. ## Layout API -### redraw +### Redraw Define `redraw` methods on panels that need to adjust themselves after their sizes, positions or visibility have been changed. Call `redraw` on `.cms-container` to re-layout the CMS. -### data-layout-type attribute +### Data-layout-type attribute Layout manager will automatically apply algorithms to the children of `.cms-container` by inspecting the `data-layout-type` attribute. Let's take the content toolbar as an example of a second-level layout application: - -```ss +```html
              - <%-- content utilising border's north, south, east, west and center classes --%> +
              ``` @@ -78,15 +77,15 @@ panel to the CMS UI. ### Methods -The following methods are available as an interface to underlying _threeColumnCompressor_ algorithm on the +The following methods are available as an interface to underlying *threeColumnCompressor* algorithm on the `.cms-container` entwine: -* **getLayoutOptions**: get currently used _threeColumnCompressor_ options. -* **updateLayoutOptions**: change specified options and trigger the laying out: +- **getLayoutOptions**: get currently used *threeColumnCompressor* options. +- **updateLayoutOptions**: change specified options and trigger the laying out: `$('.cms-container').updateLayoutOptions({mode: 'split'});` -* **splitViewMode**: enable side by side editing. -* **contentViewMode**: only menu and content areas are shown. -* **previewMode**: only menu and preview areas are shown. +- **splitViewMode**: enable side by side editing. +- **contentViewMode**: only menu and content areas are shown. +- **previewMode**: only menu and preview areas are shown. ### CSS classes @@ -95,21 +94,21 @@ If as a result of alogorithm's calculations the column becomes hidden, `column-h ## ThreeColumnCompressor You might have noticed that the top-level `.cms-container` has the `data-layout-type` set to `custom`. We use an inhouse -_threeColumnCompressor_ algorithm for the layout of the menu, content and preview columns of the CMS. The annotated code +*threeColumnCompressor* algorithm for the layout of the menu, content and preview columns of the CMS. The annotated code for this algorithm can be found in `LeftAndMain.Layout.js`. Since the layout-type for the element is set to `custom` and will be ignored by the layout manager, we apply the -_threeColumnCompressor_ explicitly `LeftAndMain::redraw`. This way we also get a chance to provide options expected +*threeColumnCompressor* explicitly `LeftAndMain::redraw`. This way we also get a chance to provide options expected by the algorithm that are initially taken from the `LeftAndMain::LayoutOptions` entwine variable. ### Layout options -* _minContentWidth_: minimum size for the content display as long as the preview is visible -* _minPreviewWidth_: preview will not be displayed below this size -* _mode_: one of "split", "content" (content-only), "preview" (preview-only) +- *minContentWidth*: minimum size for the content display as long as the preview is visible +- *minPreviewWidth*: preview will not be displayed below this size +- *mode*: one of "split", "content" (content-only), "preview" (preview-only) ## Related - * [Reference: CMS Architecture](cms_architecture) - * [Reference: Preview](preview) - * [Howto: Extend the CMS Interface](how_tos/extend_cms_interface) +- [Reference: CMS Architecture](cms_architecture) +- [Reference: Preview](preview) +- [Howto: Extend the CMS Interface](how_tos/extend_cms_interface) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md index 179f0d864..d270ae3dc 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md @@ -9,7 +9,7 @@ summary: How content previews work in the CMS With the addition of side-by-side editing, the preview has the ability to appear within the CMS window when editing content in the CMS. This is enabled by default -in the _Pages_ section for `SiteTree` models, but as outlined below can be enabled +in the *Pages* section for `SiteTree` models, but as outlined below can be enabled in other sections and for other models as well. Within the preview panel, the site is rendered into an iframe. It will update @@ -21,30 +21,34 @@ states necessary for rendering within the entwine properties. It provides function calls for transitioning between these states and has the ability to update the appearance of the option selectors. -In terms of backend support, it relies on [SilverStripeNavigator](api:SilverStripe\Admin\Navigator\SilverStripeNavigator) to be rendered -into the form. _LeftAndMain_ will automatically take care of generating it as long +In terms of backend support, it relies on [`SilverStripeNavigator`](api:SilverStripe\Admin\Navigator\SilverStripeNavigator) to be rendered +into the form. `LeftAndMain` will automatically take care of generating it as long as the `*_SilverStripeNavigator` template is found - first segment has to match the -current _LeftAndMain_-derived class (e.g. `LeftAndMain_SilverStripeNavigator`). +current *LeftAndMain*-derived class (e.g. `LeftAndMain_SilverStripeNavigator`). ## PHP + For a DataObject to be previewed using the preview panel there are a few prerequisites: - The class must implement (or [have an extension](/developer_guides/extending/extensions/) which implements) the [CMSPreviewable](api:SilverStripe\ORM\CMSPreviewable) interface - At least one preview state must be enabled for the class - There must be some valid URL to use inside the preview panel -### CMSPreviewable +### `CMSPreviewable` + The `CMSPreviewable` interface has three methods: `PreviewLink`, `CMSEditLink`, and `getMimeType`. -#### PreviewLink +#### `PreviewLink` + The `PreviewLink` method is what determines the URL used inside the preview panel. If your `DataObject` is intended to always belong to a page, you might want to preview the item in the context of where it sits on the page using an anchor. You can also provide some route specific for previewing this object, for example an action on the ModelAdmin that is used to manage the object. -#### CMSEditLink +#### `CMSEditLink` + This method exists so that when a user clicks on a link in the preview panel, the CMS edit form for the page the link leads to can be loaded. Unless your `DataObject` is [acting like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1) @@ -64,9 +68,9 @@ The easiest way to implement `CMSEditLink()` is by But for completeness, the other examples below show alternative implementations. ```php -namespace MyProject\Model; +namespace App\Model; -use MyProject\Admin\MyModelAdmin; +use App\Admin\MyModelAdmin; use SilverStripe\Admin\CMSEditLinkExtension; use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\DataObject; @@ -84,20 +88,25 @@ class MyParentModel extends DataObject implements CMSPreviewable // Get the value returned by the extension return $this->extend('CMSEditLink')[0]; } + + // ... } ``` [/hint] -#### getMimeType +#### `GetMimeType` + In ~90% of cases will be 'text/html', but note it is also possible to display (for example) an inline PDF document in the preview panel. -### Preview states +### Preview states {#preview-states-php} + The preview state(s) you apply to your `DataObject` will depend primarily on whether it uses the [Versioned](api:SilverStripe\Versioned\Versioned) extension or not. -#### Versioned DataObjects +#### Versioned `DataObject` models + If your class does use the `Versioned` extension, there are two different states available to you. It is generally recommended that you enable both, so that content authors can toggle between viewing the draft and the published content. @@ -105,16 +114,37 @@ between viewing the draft and the published content. To enable the draft preview state, use the `$show_stage_link` configuration variable. ```php -private static $show_stage_link = true; +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class MyModel extends DataObject implements CMSPreviewable +{ + private static $show_stage_link = true; + + // ... +} ``` To enable the published preview state, use the `$show_live_link` configuration variable. ```php -private static $show_live_link = true; +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class MyModel extends DataObject implements CMSPreviewable +{ + private static $show_live_link = true; + + // ... +} ``` -#### Unversioned DataObjects +#### Unversioned `DataObject` models + If you are not using the `Versioned` extension for your class, there is only one preview state you can use. This state will always be active once you enable it. @@ -122,26 +152,39 @@ To enable the unversioned preview state, use the `$show_unversioned_preview_link configuration variable. ```php -private static $show_unversioned_preview_link = true; +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class MyModel extends DataObject implements CMSPreviewable +{ + private static $show_unversioned_preview_link = true; + + // ... +} ``` -### Enabling preview for DataObjects in a ModelAdmin +### Enabling preview for `DataObject` records in a `ModelAdmin` + For this example we will take the `Product` and `MyAdmin` classes from the [ModelAdmin documentation](./modeladmin). -#### The DataObject implementation +#### The `DataObject` implementation {#modeladmin-dataobject-implementation} + As mentioned above, your `Product` class must implement the `CMSPreviewable` interface. It also needs at least one preview state enabled. This example assumes we aren't using the `Versioned` extension. ```php +namespace App\Model; + use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\DataObject; class Product extends DataObject implements CMSPreviewable { private static $show_unversioned_preview_link = true; - // ... public function PreviewLink($action = null) @@ -178,19 +221,31 @@ Note: The `if (!$this->isInDB())` check below is important! Without this, the pr `ModelAdmin` provides methods for generating a link for the correct model: ```php -public function PreviewLink($action = null) +namespace App\Model; + +use App\Admin\MyAdmin; +use SilverStripe\Control\Controller; +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class Product extends DataObject implements CMSPreviewable { - if (!$this->isInDB()) { - return null; + // ... + + public function PreviewLink($action = null) + { + if (!$this->isInDB()) { + return null; + } + $admin = MyAdmin::singleton(); + $link = Controller::join_links( + $admin->getLinkForModelClass(static::class), + 'cmsPreview', + $this->ID + ); + $this->extend('updatePreviewLink', $link, $action); + return $link; } - $admin = MyAdmin::singleton(); - $link = Controller::join_links( - $admin->getLinkForModelClass(static::class), - 'cmsPreview', - $this->ID - ); - $this->extend('updatePreviewLink', $link, $action); - return $link; } ``` @@ -201,10 +256,21 @@ If you aren't using [CMSEditLinkExtension](/developer_guides/model/managing_reco you can simply call `getCMSEditLinkForManagedDataObject()` on a singleton of the `ModelAdmin` subclass: ```php -public function CMSEditLink() +namespace App\Model; + +use App\Admin\MyAdmin; +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class Product extends DataObject implements CMSPreviewable { - $admin = MyAdmin::singleton(); - return $admin->getCMSEditLinkForManagedDataObject($this); + // ... + + public function CMSEditLink() + { + $admin = MyAdmin::singleton(); + return $admin->getCMSEditLinkForManagedDataObject($this); + } } ``` @@ -221,14 +287,26 @@ CMS what to display in the preview panel. The `forTemplate` method will probably look something like this: ```php -public function forTemplate() +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +use SilverStripe\ORM\DataObject; + +class Product extends DataObject implements CMSPreviewable { - // If the template for this DataObject is not an "Include" template, use the appropriate type here e.g. "Layout". - return $this->renderWith(['type' => 'Includes', self::class]); + // ... + + public function forTemplate() + { + // If the template for this DataObject is not an "Include" template, use the appropriate type here + // e.g. "Layout". + return $this->renderWith(['type' => 'Includes', self::class]); + } } ``` -#### The ModelAdmin implementation +#### The `ModelAdmin` implementation + We need to add the `cmsPreview` action to the `MyAdmin` class, which will output the content which should be displayed in the preview panel. @@ -237,11 +315,14 @@ in a back-end context using admin themes, it pays to ensure we're loading the fr themes whilst rendering out the preview content. ```php +namespace App\Admin; + +use App\Model\Product; use SilverStripe\Admin\ModelAdmin; use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; -class MyAdmin extends ModelAdmin +class MyAdmin extends ModelAdmin { private static $managed_models = [ Product::class, @@ -294,13 +375,13 @@ protected properties are used above (e.g. `$this->urlParams['ID']` would become [/hint] [hint] -If the css or js you have added via [the Requirements API](/developer_guides/templates/requirements/#php-requirements-api) +If the CSS or JS you have added via [the Requirements API](/developer_guides/templates/requirements/#php-requirements-api) aren't coming through, you may need to add `` and `` tags to the markup. It may not be appropriate to do this in your main template (you don't want two `` tags on a page that includes the template), so you might need a preview wrapper template, like so: -**themes/mytheme/templates/PreviewBase.ss** ```ss +<%-- themes/mytheme/templates/PreviewBase.ss --%> <%-- head tag is needed for css to be injected --%> @@ -315,31 +396,45 @@ template, like so: ``` -**in app/src/Admin/MyAdmin.php** ```php -public function cmsPreview() +// app/src/Admin/MyAdmin.php +namespace App\Admin; + +use SilverStripe\Admin\ModelAdmin; +use SilverStripe\View\ArrayData; +use SilverStripe\View\Requirements; +use SilverStripe\View\SSViewer; + +class MyAdmin extends ModelAdmin { - //... ommitted for brevity + // ... + + public function cmsPreview() + { + // ... ommitted for brevity - // Add in global css/js that would normally be added in the page base template (as needed) - Requirements::themedCSS('client/dist/css/style.css'); - // Render the preview content - $preview = $obj->forTemplate(); - // Wrap preview in proper html, body, etc so Requirements are used - $preview = SSViewer::create('PreviewBase')->process(ArrayData::create(['Preview' => $preview])); + // Add in global css/js that would normally be added in the page base template (as needed) + Requirements::themedCSS('client/dist/css/style.css'); + // Render the preview content + $preview = $obj->forTemplate(); + // Wrap preview in proper html, body, etc so Requirements are used + $preview = SSViewer::create('PreviewBase')->process(ArrayData::create(['Preview' => $preview])); - //... ommitted for brevity + // ... ommitted for brevity + } } ``` [/hint] -### Enabling preview for DataObjects which belong to a page +### Enabling preview for `DataObject` models which belong to a page + If the `DataObject` you want to preview belongs to a specific page, for example through a `has_one` or `has_many` relation, you will most likely want to preview it in the context of the page it belongs to. -#### The Page implementation +#### The page implementation + For this example we will assume the `Product` class is `Versioned`. As discussed above, the `CMSEditLink` method is used to load the correct edit form @@ -352,7 +447,7 @@ When rendering a full page in the preview panel to preview a `DataObject` on tha page, the meta tags for that page are present. When a content author toggles between the draft and published preview states, those meta tags are checked and the page's edit form would be loaded instead of the `DataObject`'s form. To avoid this -unexpected behaviour, you can include an extra GET parameter in the value returned +unexpected behaviour, you can include an extra GET parameter in the value returned by `PreviewLink`. Then in the `MetaTags` method, when the extra parameter is detected, omit the relevant meta tags. @@ -360,12 +455,16 @@ Note that this is not necessary for unversioned `DataObjects` as they only have one preview state. ```php +namespace App\PageType; + +use App\Model\Product; +use Page; use SilverStripe\Control\Controller; use SilverStripe\View\Parsers\HTMLValue; class ProductPage extends Page { - //... + // ... private static $has_many = [ 'Products' => Product::class, @@ -396,11 +495,15 @@ class ProductPage extends Page } ``` -#### The DataObject Implementation +#### The `DataObject` implementation {#page-dataobject-implementation} + Make sure the Versioned `Product` class implements `CMSPreviewable` and enables the draft and published preview states. ```php +namespace App\Model; + +use App\PageType\ProductPage; use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; @@ -408,6 +511,7 @@ use SilverStripe\Versioned\Versioned; class Product extends DataObject implements CMSPreviewable { private static $show_stage_link = true; + private static $show_live_link = true; private static $extensions = [ @@ -417,7 +521,6 @@ class Product extends DataObject implements CMSPreviewable private static $has_one = [ 'ProductPage' => ProductPage::class, ]; - // ... public function PreviewLink($action = null) @@ -436,7 +539,6 @@ class Product extends DataObject implements CMSPreviewable { return 'text/html'; } - } ``` @@ -444,32 +546,56 @@ Implement a method which gives you a unique repeatable anchor for each distinct `Product` object. ```php -/** - * Used to generate the id for the product element in the template. - */ -public function getAnchor() +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +// ... + +class Product extends DataObject implements CMSPreviewable { - return 'product-' . $this->getUniqueKey(); + // ... + + /** + * Used to generate the id for the product element in the template. + */ + public function getAnchor() + { + return 'product-' . $this->getUniqueKey(); + } } ``` For the `PreviewLink`, append the `DataObjectPreview` GET parameter to the page's frontend URL. + ```php -public function PreviewLink($action = null) +namespace App\Model; + +use SilverStripe\ORM\CMSPreviewable; +// ... + +class Product extends DataObject implements CMSPreviewable { - $link = null - if (!$this->isInDB()) { + // ... + + public function PreviewLink($action = null) + { + $link = null + if (!$this->isInDB()) { + return $link; + } + // Let the page know it's being previewed from a DataObject edit form (see Page::MetaTags()) + $action = $action . '?DataObjectPreview=' . mt_rand(); + + // Scroll the preview straight to where the object sits on the page. + $page = $this->ProductPage() + if ($page && $page->exists()) { + $link = $page->Link($action) . '#' . $this->getAnchor(); + } + + $this->extend('updatePreviewLink', $link, $action); return $link; } - // Let the page know it's being previewed from a DataObject edit form (see Page::MetaTags()) - $action = $action . '?DataObjectPreview=' . mt_rand(); - // Scroll the preview straight to where the object sits on the page. - if ($page = $this->ProductPage()) { - $link = $page->Link($action) . '#' . $this->getAnchor(); - } - $this->extend('updatePreviewLink', $link, $action); - return $link; } ``` @@ -478,7 +604,8 @@ by the `CMSPreviewable` interface so some implementation must be provided, but you can safely return `null` or an empty string with no repercussions in this situation. -#### The Page template +#### The page template + In your page template, make sure the anchor is used where you render the objects. This allows the preview panel to be scrolled automatically to where the object being edited sits on the page. @@ -492,10 +619,9 @@ being edited sits on the page. <% end_loop %> ``` +## JavaScript -## Javascript - -### Configuration and Defaults +### Configuration and defaults We use `ss.preview` entwine namespace for all preview-related entwines. @@ -506,43 +632,40 @@ In order to achieve this, create a new file `app/javascript/MyLeftAndMain.Previe In the following example we configure three aspects: - * Set the default mode from "split view" to a full "edit view" - * Make a wider mobile preview - * Increase minimum space required by preview before auto-hiding +- Set the default mode from "split view" to a full "edit view" +- Make a wider mobile preview +- Increase minimum space required by preview before auto-hiding Note how the configuration happens in different entwine namespaces ("ss.preview" and "ss"), as well as applies to different selectors -(".cms-preview" and ".cms-container"). - +(".CMS-preview" and ".CMS-container"). ```js -(function($) { - $.entwine('ss.preview', function($){ - $('.cms-preview').entwine({ - DefaultMode: 'content', - getSizes: function() { - var sizes = this._super(); - sizes.mobile.width = '400px'; - return sizes; - } - }); - }); - $.entwine('ss', function($){ - $('.cms-container').entwine({ - getLayoutOptions: function() { - var opts = this._super(); - opts.minPreviewWidth = 600; - return opts; - } - }); - }); -}(jQuery)); +jQuery.entwine('ss.preview', ($) => { + $('.cms-preview').entwine({ + DefaultMode: 'content', + getSizes() { + const sizes = this._super(); + sizes.mobile.width = '400px'; + return sizes; + } + }); +}); + +jQuery.entwine('ss', ($) => { + $('.cms-container').entwine({ + getLayoutOptions() { + const opts = this._super(); + opts.minPreviewWidth = 600; + return opts; + } + }); +}); ``` -Load the file in the CMS via setting adding 'app/javascript/MyLeftAndMain.Preview.js' +Load the file in the CMS via setting adding `app/javascript/MyLeftAndMain.Preview.js` to the `LeftAndMain.extra_requirements_javascript` [configuration value](../configuration) - ```yml SilverStripe\Admin\LeftAndMain: extra_requirements_javascript: @@ -559,7 +682,7 @@ To understand how layouts are handled in the CMS UI, have a look at the The frontend decides on the preview being enabled or disabled based on the presence of the `.cms-previewable` class. If this class is not found the preview -will remain hidden, and the layout will stay in the _content_ mode. +will remain hidden, and the layout will stay in the *content* mode. If the class is found, frontend looks for the `SilverStripeNavigator` structure and moves it to the `.cms-preview-control` panel at the bottom of the preview. @@ -572,9 +695,9 @@ The preview can be affected by calling `enablePreview` and `disablePreview`. You can check if the preview is active by inspecting the `IsPreviewEnabled` entwine property. -### Preview states +### Preview states {#preview-states-js} -States are the site stages: _live_, _stage_ etc. Preview states are picked up +States are the site stages: *live*, *stage* etc. Preview states are picked up from the `SilverStripeNavigator`. You can invoke the state change by calling: ```js @@ -582,7 +705,7 @@ $('.cms-preview').entwine('.ss.preview').changeState('StageLink'); ``` Note the state names come from [SilverStripeNavigatorItem](api:SilverStripe\Admin\Navigator\SilverStripeNagivatorItem) class names - thus -the _Link_ in their names. This call will also redraw the state selector to fit +the `Link` in their names. This call will also redraw the state selector to fit with the internal state. See `AllowedStates` in `.cms-preview` entwine for the list of supported states. @@ -598,10 +721,10 @@ This selector defines how the preview iframe is rendered, and try to emulate different device sizes. The options are hardcoded. The option names map directly to CSS classes applied to the `.cms-preview` and are as follows: -* _auto_: responsive layout -* _desktop_ -* _tablet_ -* _mobile_ +- *auto*: responsive layout +- *desktop* +- *tablet* +- *mobile* You can switch between different types of display sizes programmatically, which has the benefit of redrawing the related selector and maintaining a consistent @@ -619,7 +742,7 @@ $('.cms-preview').entwine('.ss.preview').getCurrentSizeName(); ### Preview modes -Preview modes map to the modes supported by the _threeColumnCompressor_ layout +Preview modes map to the modes supported by the *threeColumnCompressor* layout algorithm, see [layout reference](cms_layout) for more details. You can change modes by calling: @@ -646,16 +769,16 @@ option selectors, even if they try to appear as one horizontal bar. Namespace `ss.preview`, selector `.cms-preview`: -* **getCurrentStateName**: get the name of the current state (e.g. _LiveLink_ or _StageLink_). -* **getCurrentSizeName**: get the name of the current device size. -* **getIsPreviewEnabled**: check if the preview is enabled. -* **changeState**: one of the `AllowedStates`. -* **changeSize**: one of _auto_, _desktop_, _tablet_, _mobile_. -* **changeMode**: maps to _threeColumnLayout_ modes - _split_, _preview_, _content_. -* **enablePreview**: activate the preview and switch to the _split_ mode. Try to load the relevant URL from the content. -* **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new +- **getCurrentStateName**: get the name of the current state (e.g. *LiveLink* or *StageLink*). +- **getCurrentSizeName**: get the name of the current device size. +- **getIsPreviewEnabled**: check if the preview is enabled. +- **changeState**: one of the `AllowedStates`. +- **changeSize**: one of *auto*, *desktop*, *tablet*, *mobile*. +- **changeMode**: maps to *threeColumnLayout* modes - *split*, *preview*, *content*. +- **enablePreview**: activate the preview and switch to the *split* mode. Try to load the relevant URL from the content. +- **disablePreview**: deactivate the preview and switch to the *content* mode. Preview will re-enable itself when new previewable content is loaded. ### Related - * [Reference: Layout](cms_layout) +- [Reference: Layout](cms_layout) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md index 99747523c..bbea283a2 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md @@ -4,12 +4,12 @@ summary: Add custom CSS properties to the rich-text editor. icon: text-width --- -# WYSIWYG Styles +# WYSIWYG styles Silverstripe CMS lets you customise the style of content in the CMS. This is done by setting up a CSS file called -`editor.css` in either your theme or in your `app/` folder. This is set through yaml config: +`editor.css` in either your theme or in your `app/` folder. This is set through YAML config: -```yaml +```yml --- name: MyCSS --- @@ -28,7 +28,7 @@ $config->setContentCSS([ '/app/client/css/editor.css' ]); ``` [notice] -`silverstripe/admin` adds a small css file to `editor_css` which highlights broken links - you'll +`silverstripe/admin` adds a small CSS file to `editor_css` which highlights broken links - you'll probably want to include that in the array you pass to `setContentCSS()`, either by first calling `getContentCSS()` and merging that array with your new one (and passing the result to `setContentCSS()`) or by adding `'/_resources/vendor/silverstripe/admin/client/dist/styles/editor.css'` to the array you pass @@ -37,7 +37,7 @@ to `setContentCSS()` ## Custom style dropdown -The custom style dropdown can be enabled via the `importcss` plugin bundled with admin module. ([Doc](https://www.tiny.cloud/docs/tinymce/6/importcss/)) +The custom style dropdown can be enabled via the `importcss` plugin bundled with admin module. ([Doc](https://www.tiny.cloud/docs/tinymce/6/importcss/)) Use the below code in `app/_config.php`: ```php @@ -48,7 +48,7 @@ TinyMCEConfig::get('cms') ->setOption('importcss_append', true); ``` -Any CSS classes within this file will be automatically added to the `WYSIWYG` editors 'style' dropdown. +Any CSS classes within this file will be automatically added to the `WYSIWYG` editors 'style' dropdown. For instance, to add the color 'red' as an option within the `WYSIWYG` add the following to the `editor.css` @@ -58,7 +58,7 @@ add the color 'red' as an option within the `WYSIWYG` add the following to the ` } ``` -Adding a tag to the selector will automatically wrap with this tag. For example: +Adding a tag to the selector will automatically wrap with this tag. For example: ```css h4.red { @@ -68,10 +68,10 @@ h4.red { will add an `h4` tag to the selected block. -For further customisation, customize the `style_formats` option. -`style_formats` won't be applied if you do not enable `importcss_append`. +For further customisation, customize the `style_formats` option. +`style_formats` won't be applied if you do not enable `importcss_append`. Here is a working example to get you started.   -See related [tinymce doc](https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#style_formats). +See related [TinyMCE doc](https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#style_formats). ```php use SilverStripe\Forms\HTMLEditor\TinyMCEConfig; @@ -85,11 +85,11 @@ $formats = [ ['title' => 'Heading 5', 'block' => 'h5' ], ['title' => 'Heading 6', 'block' => 'h6' ], [ - 'title' => 'Subtitle', - 'selector' => 'p', - 'classes' => 'title-sub', + 'title' => 'Subtitle', + 'selector' => 'p', + 'classes' => 'title-sub', ], - ] + ], ], [ 'title' => 'Misc Styles', 'items' => [ @@ -106,7 +106,7 @@ $formats = [ 'classes' => 'btn-red', 'merge_siblings' => true, ], - ] + ], ], ]; @@ -118,6 +118,7 @@ TinyMCEConfig::get('cms') ]); ``` -## API Documentation +## API documentation -* [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) +- [`HtmlEditorConfig`](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) +- [`TinyMCEConfig`](api:SilverStripe\Forms\HTMLEditor\TinyMCEConfig) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md index 9c6b5d4a6..a145fac1e 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md @@ -4,22 +4,22 @@ summary: Advanced documentation about writing and customizing javascript within iconBrand: js --- -# Javascript Development +# JavaScript development -The following document is an advanced guide on building rich javascript interactions within the Silverstripe CMS and -a list of our best practices for contributing and modifying the core javascript framework. +The following document is an advanced guide on building rich JavaScript interactions within the Silverstripe CMS and +a list of our best practices for contributing and modifying the core JavaScript framework. ## Build tools -Silverstripe's javascript is transpiled, meaning it goes through a transformative process that takes our original source code -and outputs javascript that is more efficient, smaller in overall file size, and works on a wider range of browsers. +Silverstripe's JavaScript is transpiled, meaning it goes through a transformative process that takes our original source code +and outputs JavaScript that is more efficient, smaller in overall file size, and works on a wider range of browsers. There are many ways to solve the problem of transpiling. The toolchain we use in core Silverstripe CMS modules includes: -* [Babel](https://babeljs.io) (transpiler) -* [Webpack](https://webpack.js.org) (Module bundler) +- [Babel](https://babeljs.io) (transpiler) +- [Webpack](https://webpack.js.org) (Module bundler) -## jQuery, jQuery UI and jQuery.entwine: Our libraries of choice +## jQuery, jQuery UI and jQuery.Entwine: our libraries of choice [notice] The following documentation regarding jQuery, jQueryUI and Entwine does not apply to React components or sections powered by React. @@ -36,7 +36,7 @@ Silverstripe CMS uses [jQuery UI](https://ui.jquery.com) on top of jQuery. For any custom code developed with jQuery, you have four choices to structure it: Custom jQuery Code, a jQuery Plugin, a jQuery UI Widget, or a `jQuery.entwine` behaviour. We'll detail below where each solution is appropriate. -### Custom jQuery Code +### Custom jQuery code jQuery allows you to write complex behavior in a couple of lines of JavaScript. Smaller features which aren't likely to be reused can be custom code without further encapsulation. For example, a button rollover effect doesn't require a full @@ -44,32 +44,31 @@ plugin. See "[How jQuery Works](https://docs.jquery.com/How_jQuery_Works)" for a You should write all your custom jQuery code in a closure. - ```js -(function($) { - $(document).ready(function(){ - // your code here. - }) -})(jQuery); +(function ($) { + $(document).ready(() => { + // your code here. + }); +}(jQuery)); ``` -#### jQuery Plugins +#### jQuery plugins A jQuery Plugin is essentially a method call which can act on a collection of DOM elements. It is contained within the `jQuery.fn` namespace, and attaches itself automatically to all jQuery collections. You can read more about these, including how to create your own plugins, in the official [jQuery Plugins](https://learn.jquery.com/plugins/) documentation. -#### jQuery UI Widgets +#### jQuery UI widgets UI Widgets are jQuery Plugins with a bit more structure, targeted towards interactive elements. They require jQuery and the core libraries in jQuery UI, so are generally more heavyweight if jQuery UI isn't already used elsewhere. Main advantages over simpler jQuery plugins are: -* Exposing public methods on DOM elements (incl. pseudo-private methods) -* Exposing configuration and getters/setters on DOM elements -* Constructor/Destructor hooks -* Focus management and mouse interaction +- Exposing public methods on DOM elements (incl. pseudo-private methods) +- Exposing configuration and getters/setters on DOM elements +- Constructor/Destructor hooks +- Focus management and mouse interaction See the [official API documentation](https://api.jqueryui.com/) and read up about the [jQuery UI Widget Factory](https://learn.jquery.com/jquery-ui/widget-factory/) to get started. @@ -82,25 +81,23 @@ Use jQuery.entwine when your code is likely to be customised by others, or when Example: Highlighter - ```js -(function($) { +(function ($) { $(':button').entwine({ Foreground: 'red', Background: 'yellow', - highlight: function() { + highlight() { this.css('background', this.getBackground()); this.css('color', this.getForeground()); } }); -})(jQuery); +}(jQuery)); ``` Usage: - ```js -(function($) { +(function ($) { // call with default options $(':button').entwine().highlight(); @@ -109,14 +106,14 @@ Usage: // get property $(':button').entwine().getBackground(); -})(jQuery); +}(jQuery)); ``` This is a deliberately simple example, the strength of jQuery.entwine over simple jQuery plugins lies in its public properties, namespacing, as well as its inheritance based on CSS selectors. Go to our [jQuery Entwine documentation](jquery_entwine) for more complete examples. -## Architecture and Best Practices +## Architecture and best practices ### Keep things simple @@ -127,19 +124,18 @@ jQuery with a few lines of code. Your jQuery code will normally end up as a seri Global properties are evil. They are accessible by other scripts and might be overwritten or misused. A popular case is the `$` shortcut in different libraries: in PrototypeJS it stands for `document.getElementByID()`, in jQuery for `jQuery()`. - ```js // you can't rely on '$' being defined outside of the closure -(function($) { - var myPrivateVar; // only available inside the closure +(function ($) { + let myPrivateVar; // only available inside the closure // inside here you can use the 'jQuery' object as '$' -})(jQuery); +}(jQuery)); ``` -You can run `[jQuery.noConflict()](https://docs.jquery.com/Core/jQuery.noConflict)` to avoid namespace clashes. -NoConflict mode is enabled by default in the Silverstripe CMS javascript. +You can run [`jQuery.noConflict()`](https://docs.jquery.com/Core/jQuery.noConflict) to avoid namespace clashes. +NoConflict mode is enabled by default in the Silverstripe CMS JavaScript. -### Initialize at document.ready +### Initialize at document.Ready You have to ensure that DOM elements you want to act on are loaded before using them. jQuery provides a wrapper around the `window.onload` and `document.ready` events. @@ -149,12 +145,12 @@ This doesn't apply to jQuery entwine declarations, which will apply to elements [/info] ```js -(function($) { +(function ($) { // DOM elements might not be available here - $(document).ready(function() { + $(document).ready(() => { // The DOM is fully loaded here }); -})(jQuery); +}(jQuery)); ``` See [the jQuerydocs on `$( document ).ready()`](https://learn.jquery.com/using-jquery-core/document-ready/). @@ -170,12 +166,12 @@ Example: Add a 'loading' classname to all pressed buttons ```js // manual binding, only applies to existing elements -$('input[[type=submit]]').on('click', function() { +$('input[[type=submit]]').on('click', function () { $(this).addClass('loading'); }); // binding, applies to any inserted elements as well -$('.cms-container').on('click', 'input[[type=submit]]', function() { +$('.cms-container').on('click', 'input[[type=submit]]', function () { $(this).addClass('loading'); }); ``` @@ -184,14 +180,13 @@ $('.cms-container').on('click', 'input[[type=submit]]', function() { You can do this using entwine as well, which has the added benefit of not requiring your original selector to match a DOM element initially (e.g. for the above example if there are no `.cms-container` elements, or those elements are removed and re-added to the DOM, your native binding won't work but an entwine one will). [/hint] -### Assume Element Collections +### Assume element collections jQuery is based around collections of DOM elements, the library functions typically handle multiple elements (where it makes sense). Encapsulate your code by nesting your jQuery commands inside a `jQuery().each()` call. - ```js -$('div.MyGridField').each(function() { +$('div.MyGridField').each(function () { // This is the over code for the tr elements inside a GridField. $(this).find('tr').hover( // ... @@ -199,38 +194,42 @@ $('div.MyGridField').each(function() { }); ``` -### Use plain HTML and jQuery.data() to store data +### Use plain HTML and `jQuery.data()` to store data -The DOM can make javascript configuration and state-keeping a lot easier, without having to resort to javascript +The DOM can make JavaScript configuration and state-keeping a lot easier, without having to resort to JavaScript properties and complex object graphs. Example: Simple form change tracking to prevent submission of unchanged data Through CSS properties - ```js -$('form :input').bind('change', function(e) { +$('form :input').bind('change', function (event) { $(this.form).addClass('isChanged'); }); -$('form').bind('submit', function(e) { - if($(this).hasClass('isChanged')) return false; + +$('form').bind('submit', function (event) { + if ($(this).hasClass('isChanged')) { + event.preventDefault(); + } }); ``` -Through jQuery.data() +Through `jQuery.data()` ```js -$('form :input').bind('change', function(e) { +$('form :input').bind('change', function (event) { $(this.form).data('isChanged', true); }); -$('form').bind('submit', function(e) { - alert($(this).data('isChanged')); - if($(this).data('isChanged')) return false; + +$('form').bind('submit', function (event) { + if ($(this).data('isChanged')) { + event.preventDefault(); + } }); ``` -### Return HTML/JSON and HTTPResponse class for AJAX responses +### Return HTML/JSON and `HTTPResponse` class for AJAX responses Ajax responses will sometimes need to update existing DOM elements, for example refresh a set of search results. Returning plain HTML is generally a good default behaviour, as it allows you to keep template rendering in one place (in @@ -248,7 +247,6 @@ Example: Autocomplete input field loading page matches through AJAX Template: - ```ss
                <% loop $Results %> @@ -259,38 +257,44 @@ Template: PHP: - ```php +namespace App\Control; + +use Page; use SilverStripe\Control\HTTPResponse; use SilverStripe\View\ViewableData; -class MyController +class MyController { - public function autocomplete($request) - { - $results = Page::get()->filter("Title", $request->getVar('title')); - if(!$results) return new HTTPResponse("Not found", 404); - - // Use HTTPResponse to pass custom status messages - $this->getResponse() - ->setStatusCode(200) - ->addHeader('X-Status', "Found " . $results->Count() . " elements"); - - // render all results with a custom template - $vd = new ViewableData(); - return $vd->customise([ - "Results" => $results - ])->renderWith('AutoComplete'); - } + private static $url_segment = 'my_controller'; + // ... + + public function autocomplete($request) + { + $results = Page::get()->filter('Title', $request->getVar('title')); + if (!$results) { + return HTTPResponse::create('Not found', 404); + } + + // Use HTTPResponse to pass custom status messages + $this->getResponse() + ->setStatusCode(200) + ->addHeader('X-Status', 'Found ' . $results->Count() . ' elements'); + + // render all results with a custom template + $vd = ViewableData::create(); + return $vd->customise([ + 'Results' => $results, + ])->renderWith('AutoComplete'); + } } ``` HTML - ```ss -
                +
                @@ -300,22 +304,21 @@ HTML JavaScript: - ```js -$('.autocomplete input').on('change', function() { - var resultsEl = $(this).siblings('.results'); +$('.autocomplete input').on('change', function () { + const resultsEl = $(this).siblings('.results'); resultsEl.load( // get form action, using the jQuery.metadata plugin $(this).parent().metadata().url, // submit all form values $(this.form).serialize(), // callback after data is loaded - function(data, status) { + (data, status) => { resultsEl.show(); // get all record IDs from the new HTML - var ids = jQuery('.results').find('li').map(function() { - return $(this).attr('id').replace(/Record\-/,''); - }); + const ids = jQuery('.results').find('li').map( + () => $(this).attr('id').replace(/Record\-/, '') + ); } ); }); @@ -329,7 +332,7 @@ properly set. ### Use events and observation to link components together -The philosophy behind this javascript guide is **component driven development**: your javascript should be structured as +The philosophy behind this JavaScript guide is **component driven development**: your JavaScript should be structured as a set of components that communicate. Event handlers are a great way of getting components to community, as long as two-way communication isn't required. Set up a number of custom event names that your component will trigger. List them in the component documentation comment. @@ -340,29 +343,31 @@ events](https://docs.jquery.com/Namespaced_Events). Example: Trigger custom 'validationfailed' event on form submission for each empty element - ```js -$('form').on('submit', function(e) { +$('form').on('submit', function (e) { // $(this) refers to form - $(this).find(':input').each(function() { + $(this).find(':input').each(function () { // $(this) in here refers to input field - if(!$(this).val()) $(this).trigger('validationfailed'); + if (!$(this).val()) { + $(this).trigger('validationfailed'); + } }); + return false; }); // listen to custom event on each field -$('form :input').on('validationfailed', function(e) { +$('form :input').on('validationfailed', function (e) { // $(this) refers to input field - alert($(this).attr('name')); + const fieldName = $(this).attr('name'); }); ``` Don't use event handlers in the following situations: -* If two-way communication is required, for example, calling an method in another component, which returns data that +- If two-way communication is required, for example, calling an method in another component, which returns data that you then use. Event handlers can't have return values. -* If specific execution order is required. Event handlers are executed in parallel, which makes it difficult to know +- If specific execution order is required. Event handlers are executed in parallel, which makes it difficult to know the exact order in which code in different threads will execute. If the execution order is likely to cause problems, it is better to use a code structure that is executed sequentially. An example might be two events modifying the same piece of the DOM. @@ -374,14 +379,14 @@ advantage is that they lack the two problems listed in bullets just above. The d need to define an custom API for configuring the callbacks; whereas, event observation is a jQuery provided API that leaves components very loosely coupled. -### Use jQuery.entwine to define APIs as necessary +### Use jQuery.Entwine to define APIs as necessary By default, most of your JavaScript methods will be hidden in closures like a jQuery plugin, and are not accessible from the outside. As a best practice, each jQuery plugin should only expose one method to initialize and configure it. If you need more public methods, consider using either a jQuery UI Widget, or define your behaviour as jQuery.entwine rules (see above). -### Write Documentation +### Write documentation Documentation in JavaScript usually resembles the JavaDoc standard, although there is no agreed standard. Due to the flexibility of the language it can be hard to generate automated documentation, particularly with the predominant usage @@ -410,37 +415,35 @@ Example: jQuery.entwine * @name ss.LeftAndMain */ $('.LeftAndMain').entwine({ - /** - * Reference to some property - * @type Number - */ - MyProperty: 123, - - /** - * Renders the provided data into an unordered list. - * - * @param {Object} data - * @param {String} status - * @return {String} HTML unordered list - */ - publicMethod: function(data, status) { - return '
                  ' - + //... - + '
                '; - }, - - /** - * Won't show in documentation, but still worth documenting. - * - * @return {String} Something else. - */ - _privateMethod: function() { - // ... - } + /** + * Reference to some property + * @type Number + */ + MyProperty: 123, + + /** + * Renders the provided data into an unordered list. + * + * @param {Object} data + * @param {String} status + * @return {String} HTML unordered list + */ + publicMethod(data, status) { + return '
                  ...
                '; + }, + + /** + * Won't show in documentation, but still worth documenting. + * + * @return {String} Something else. + */ + _privateMethod() { + // ... + } }); ``` ## Related -* [Unobtrusive Javascript](https://www.onlinetools.org/articles/unobtrusivejavascript/chapter1.html) -* [Quirksmode: In-depth Javascript Resources](https://www.quirksmode.org/resources.html) +- [Unobtrusive JavaScript](https://www.onlinetools.org/articles/unobtrusivejavascript/chapter1.html) +- [Quirksmode: In-depth JavaScript Resources](https://www.quirksmode.org/resources.html) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md index 3baaea44d..ea41afb0c 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md @@ -3,7 +3,7 @@ title: jQuery Entwine iconBrand: js --- -# jQuery Entwine +# jQuery entwine [notice] The following documentation regarding jQuery and Entwine does not apply to React components or sections powered by React. @@ -28,14 +28,14 @@ $('div').entwine({ ``` [info] -The definitions you provide are _not_ bound to the elements that match at definition time. You can declare behaviour prior to the DOM existing in any form (i.e. prior to DOMReady) and later calls and event handlers will function correctly. +The definitions you provide are *not* bound to the elements that match at definition time. You can declare behaviour prior to the DOM existing in any form (i.e. prior to DOMReady) and later calls and event handlers will function correctly. [/info] ### Selector specifity and "inheritance" {#specificity} -When there are two definitions for an event handler, method, or property on a particular DOM node, only the function with the most _specific_ selector is used. +When there are two definitions for an event handler, method, or property on a particular DOM node, only the function with the most *specific* selector is used. -_Specifity_ is calculated as defined by the CSS 2/3 spec. This can be seen as _subclassing_ applied to behaviour. This is determined by the selector used for _defining_ the entwine logic, _not_ the selector used to select the DOM element. +*Specifity* is calculated as defined by the CSS 2/3 spec. This can be seen as *subclassing* applied to behaviour. This is determined by the selector used for *defining* the entwine logic, *not* the selector used to select the DOM element. For example, given this DOM structure @@ -51,19 +51,21 @@ And this entwine definition ```js $('div').entwine({ - foo: function() { + foo() { + // eslint-disable-next-line no-console console.log(this.text()); }, }); $('.attribute_text').entwine({ - foo: function() { + foo() { + // eslint-disable-next-line no-console console.log(this.attr('rel')); }, }); ``` -Then this call, which only matches (and therefore only calls) the method for the element with the `attribute_text` css class +Then this call, which only matches (and therefore only calls) the method for the element with the `attribute_text` CSS class ```js $('.attribute_text').foo(); @@ -71,7 +73,7 @@ $('.attribute_text').foo(); Will log this to the console -``` +```text Attribute text ``` @@ -83,19 +85,19 @@ $('div').foo(); Will log this to the console -``` +```text Internal text Attribute text Nonsense ``` [notice] -For selectors with _the same_ level of specificity, the definition which is declared first takes precedence. +For selectors with *the same* level of specificity, the definition which is declared first takes precedence. [/notice] #### Calling less-specific logic from a definition with higher-specificity -There may be times when you want to apply _additional_ logic to a method or event handler for a given DOM element, but still call the logic for the lower-specificity declaration. For example you might want to perform some conditional check before allowing a button click event to occur. +There may be times when you want to apply *additional* logic to a method or event handler for a given DOM element, but still call the logic for the lower-specificity declaration. For example you might want to perform some conditional check before allowing a button click event to occur. You can call the logic for the declaration with lower-specificity by calling `this._super()`. This special function can take any arguments, and will pass them on to the appropriate method or event handler. @@ -103,13 +105,15 @@ For example, with the following entwine definition ```js $('a').entwine({ - onclick: function(e) { + onclick(e) { + // eslint-disable-next-line no-console console.log('clicked the link element'); }, }); $('.btn').entwine({ - onclick: function(e) { + onclick(e) { + // eslint-disable-next-line no-console console.log('clicked the .btn element'); this._super(e); }, @@ -118,7 +122,7 @@ $('.btn').entwine({ Clicking a `
                ` element will log this to the console -``` +```text clicked the .btn element clicked the link element ``` @@ -130,9 +134,9 @@ If the `this._super()` call was removed, the event would never be passed on to t The jQuery object that entwine is called on must be selected using a plain selector, without context. These examples will not work: ```js -$('div', el).entwine(/*...*/) -$([ela, elb, elc]).entwine(/*...*/) -$('
                ').entwine(/*...*/) +$('div', el).entwine(/* ... */); +$([ela, elb, elc]).entwine(/* ... */); +$('
                ').entwine(/* ... */); ``` ## Adding methods to DOM elements @@ -141,11 +145,11 @@ To attach methods to DOM nodes, call the `entwine` function on a jQuery selector ```js $('div').entwine({ - foo: function(args) { + foo(args) { // Some logic here }, - bar: function(args) { + bar(args) { // Some logic here }, }); @@ -176,13 +180,15 @@ And this entwine definition ```js $('.internal_text').entwine({ - foo: function() { + foo() { + // eslint-disable-next-line no-console console.log(this.text()); }, }); $('.attribute_text').entwine({ - foo: function() { + foo() { + // eslint-disable-next-line no-console console.log(this.attr('rel')); }, }); @@ -196,12 +202,12 @@ $('div').foo(); Will log this to the console -``` +```text Internal text Attribute text ``` -Note that it is calling the `foo()` method on _both_ divs, and that each had a different `foo()` method defined based on different selectors. +Note that it is calling the `foo()` method on *both* divs, and that each had a different `foo()` method defined based on different selectors. ## Events @@ -217,14 +223,14 @@ name. Just like other functions this binding will be live, and only the most spe ```js /* No need for onready wrapper. Events are bound as needed */ $('div').entwine({ - onclick: function() { - this.css({backgroundColor: 'blue'}); + onclick() { + this.css({ backgroundColor: 'blue' }); }, }); $('.green').entwine({ - onclick: function() { - this.css({color: 'green'}); + onclick() { + this.css({ color: 'green' }); }, }); ``` @@ -247,21 +253,21 @@ Examples of where this can be useful are if the logic for the element the events ```js $('div').entwine({ 'from a': { - onclick: function() { - this.css({color: 'green'}); + onclick() { + this.css({ color: 'green' }); this._super(); }, }, }); ``` -## Constructors / Destructors +## Constructors / destructors Declaring a function with the name `onmatch` will create a behavior that is called on each object when it matches. Likewise, `onunmatch` will be called when an object that did match this selector stops matching it (because it is removed, or because you've changed its properties). -Note that an onunmatch block must be paired with an onmatch block - an onunmatch without an onmatch _in the same entwine definition block_ is illegal. +Note that an onunmatch block must be paired with an onmatch block - an onunmatch without an onmatch *in the same entwine definition block* is illegal. -You can also declare a function with the name `onadd` which is similar to `onmatch` but is explicitly triggered by the element being added to the DOM. This means if the element already exists when you declare this function, your function will not be called (but `onmatch` would be). Similarly, if you delcare a function called `onremove`, it will be called when an element is _removed_ from the DOM. This does not need an `onadd` function to be declared, unlike `onunmatch`. +You can also declare a function with the name `onadd` which is similar to `onmatch` but is explicitly triggered by the element being added to the DOM. This means if the element already exists when you declare this function, your function will not be called (but `onmatch` would be). Similarly, if you delcare a function called `onremove`, it will be called when an element is *removed* from the DOM. This does not need an `onadd` function to be declared, unlike `onunmatch`. [warning] The `onmatch` and `onadd` events are triggered `asynchronously` - this means that after you add an element to the DOM, it is not guaranteed that functionality in your `onmatch` or `onadd` function for that element will be processed immediately. This is handled using a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). @@ -296,9 +302,9 @@ Most entwine logic defined in core Silverstripe CMS modules uses the `ss` namesp To avoid name clashes, to allow multiple bindings to the same event, and to generally seperate a set of functions from other code, you can use namespaces. These are declared by calling the `jQuery.entwine()` function and passing in both the namespace name and a callback, which contains all entwine declarations which belong to that namespace: ```js -$.entwine('foo.bar', function($) { +$.entwine('foo.bar', ($) => { $('div').entwine({ - baz: function() { + baz() { // Some logic here } }); @@ -308,14 +314,14 @@ $.entwine('foo.bar', function($) { You can then call these functions like this: ```js -$('div').entwine('foo.bar').baz() +$('div').entwine('foo.bar').baz(); ``` [info] -Notice that `$` is passed in as an argument to the callback function. This is a _different object_ than the `$` which the `entwine()` function is being called on, which contains information about the namespace that you have defined. Another way to write the namespace closure, which illustrates this point, would be like so: +Notice that `$` is passed in as an argument to the callback function. This is a *different object* than the `$` which the `entwine()` function is being called on, which contains information about the namespace that you have defined. Another way to write the namespace closure, which illustrates this point, would be like so: ```js -jQuery.entwine('foo.bar', function($) { +jQuery.entwine('foo.bar', ($) => { $('div').entwine({ // declarations here }); @@ -328,15 +334,15 @@ Namespaced functions, properties, and event handlers work just like regular func ```js $('div').entwine({ - onclick: function() { - this.css({backgroundColor: 'blue'}); + onclick() { + this.css({ backgroundColor: 'blue' }); }, }); -$.entwine('foo', function($) { +$.entwine('foo', ($) => { $('div').entwine({ - onclick: function() { - this.css({color: 'green'}); + onclick() { + this.css({ color: 'green' }); }, }); }); @@ -348,29 +354,31 @@ This is particularly important when writing reusable code, since otherwise you c Although a namespace can be any string, best practise is to name them with dotted-identifier notation. For example, the entwine logic for [controlling the preview panel in the CMS](/developer_guides/customising_the_admin_interface/preview/#javascript) uses the `ss.preview` namespace. -### Namespaces and scope (or What the hell's up with that ugly function closure) {#namespaces-and-scope} +### Namespaces and scope (or what the hell's up with that ugly function closure) {#namespaces-and-scope} Inside a namespace definition, functions remember the namespace they are in, and calls to other functions will be looked up inside that namespace first. Where they don't exist (see warning below), they will be looked up in the base namespace ```js -$.entwine('foo', function($) { +$.entwine('foo', ($) => { $('div').entwine({ - bar: function() { + bar() { this.baz(); this.qux(); }, - baz: function() { + baz() { + // eslint-disable-next-line no-console console.log('baz'); }, - }) -}) + }); +}); $('div').entwine({ - qux: function() { + qux() { + // eslint-disable-next-line no-console console.log('qux'); }, -}) +}); ``` With the above entwine declarations, calling @@ -381,17 +389,17 @@ $('div').entwine('foo').bar(); Will print this to the console: -``` +```text baz qux ``` [info] -Note that trying to call `$('div').bar();` would throw an uncaught `TypeError` saying something like "$(...).bar is not a function", because the `bar()` function was defined in a namespace, but we are trying to call that function from _outside_ of that namespace. +Note that trying to call `$('div').bar();` would throw an uncaught `TypeError` saying something like "$(...).bar is not a function", because the `bar()` function was defined in a namespace, but we are trying to call that function from *outside* of that namespace. [/info] [warning] -Note that 'exists' means that a function is declared in this namespace for _any_ selector, not just a matching one. Given the dom +Note that 'exists' means that a function is declared in this namespace for *any* selector, not just a matching one. Given the dom ```html
                Internal text
                @@ -400,28 +408,30 @@ Note that 'exists' means that a function is declared in this namespace for _any_ And the entwine definitions ```js -$.entwine('foo', function($) { +$.entwine('foo', ($) => { $('div').entwine({ - bar: function() { + bar() { this.baz(); }, }); $('span').entwine({ - baz: function() { + baz() { + // eslint-disable-next-line no-console console.log('a'); }, }); -}) +}); $('div').entwine({ - baz: function() { + baz() { + // eslint-disable-next-line no-console console.log('b'); }, }); ``` -Then calling `$('div')entwine('foo').bar();` will _not_ display "b". Even though the `span` rule could never match a `div`, because `baz()` is defined for some rule in the `foo` namespace, the base namespace will never be checked. +Then calling `$('div')entwine('foo').bar();` will *not* display "b". Even though the `span` rule could never match a `div`, because `baz()` is defined for some rule in the `foo` namespace, the base namespace will never be checked. [/warning] ### Calling to another namespace (and forcing base) @@ -431,32 +441,32 @@ Inside a namespace, namespace lookups are by default relative to the current nam In some situations (such as the last example) you may want to force using the base namespace. In this case you can call entwine with the first argument being the base namespace code `'.'`. For example, if the first definition in the previous example was ```js -$.entwine('foo', function($) { +$.entwine('foo', ($) => { $('div').entwine({ - bar: function() { + bar() { this.entwine('.').baz(); }, - }) -}) + }); +}); ``` -Then "b" _would_ be output to the console. +Then "b" *would* be output to the console. ### Nesting namespace blocks You can also nest namespace declarations. In this next example, we're defining the functions `$().entwine('zap').bar()` and `$().entwine('zap.pow').baz()` ```js -$.entwine('zap', function($) { +jQuery.entwine('zap', ($) => { $('div').entwine({ - bar: function() { + bar() { // Some logic here }, }); - $.entwine('pow', function($) { - $('div').entwine({ - baz: function() { + $.entwine('pow', ($jq) => { + $jq('div').entwine({ + baz() { // Some logic here }, }); @@ -466,10 +476,10 @@ $.entwine('zap', function($) { ### Using -Sometimes a block outside of a namespace will need to refer to that namespace repeatedly. By passing a _function_ (instead of an object) to the entwine function, you can change the looked-up namespace. +Sometimes a block outside of a namespace will need to refer to that namespace repeatedly. By passing a *function* (instead of an object) to the entwine function, you can change the looked-up namespace. ```js -$('div').entwine('foo', function($) { +$('div').entwine('foo', function ($) { this.bar(); this.bar(); this.bar(); @@ -487,4 +497,4 @@ div.bar(); Both of the above implementations repeatedly call the `bar()` method which was declared in the `foo` entwine namespace on the element matching `div`. -This is equivalent to the (deprecated) [`with` feature in javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with). Care should be taken to only use this construct in situations that merit it. +This is equivalent to the (deprecated) [`with` feature in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with). Care should be taken to only use this construct in situations that merit it. diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md index 4ee5da9c4..b162e19d5 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md @@ -6,23 +6,24 @@ iconBrand: react # Introduction to the "React" layer -Some admin modules render their UI with React, a popular Javascript library created by Facebook. +Some admin modules render their UI with React, a popular JavaScript library created by Facebook. For these sections, rendering happens via client side scripts that create and inject HTML declaratively using data structures. -Even within sections that are _not_ primarily rendered in react, several React components may be injected into the DOM. +Even within sections that are *not* primarily rendered in react, several React components may be injected into the DOM. There are some several members of this ecosystem that all work together to provide a dyanamic UI. They include: -* [ReactJS](https://react.dev/) - A Javascript UI library -* [Redux](https://redux.js.org/) - A state manager for Javascript -* [GraphQL](https://graphql.org/) - A query language for your API -* [Apollo Client](https://www.apollographql.com/apollo-client) - A framework for using GraphQL in your application + +- [ReactJS](https://react.dev/) - A JavaScript UI library +- [Redux](https://redux.js.org/) - A state manager for JavaScript +- [GraphQL](https://graphql.org/) - A query language for your API +- [Apollo Client](https://www.apollographql.com/apollo-client) - A framework for using GraphQL in your application All of these pillars of the frontend application can be customised, giving you more control over how the admin interface looks, feels, and behaves. [alert] These technologies underpin the future of Silverstripe CMS development, but their current implementation is -_experimental_. Our APIs are not expected to change drastically between releases, but they are excluded from +*experimental*. Our APIs are not expected to change drastically between releases, but they are excluded from our [semantic versioning](https://semver.org) commitments for the time being. Any breaking changes will be clearly signalled in release notes. [/alert] @@ -34,12 +35,16 @@ First, a brief summary of what each of these are: React's job is to render UI. Its UI elements are known as "components" and represent the fundamental building block of a React-rendered interface. A React component expressed like this: ```js - - - +import React from 'react'; +// ... + + + + ; ``` Might actually render HTML that looks like this: + ```html
                @@ -51,53 +56,54 @@ Might actually render HTML that looks like this:
                ``` -This syntax is known as JSX. It is transpiled at build time into native Javascript calls +This syntax is known as JSX. It is transpiled at build time into native JavaScript calls to the React API. While optional, it is recommended to express components this way. -### Recommended: React Dev Tools +### Recommended: react dev tools -The [React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) extension available for Chrome and Firefox is critical to debugging a React UI. It will let you browse the React UI much like the DOM, showing the tree of rendered components and their current props and state in real time. +The [React Dev Tools](https://chrome.g.oogle.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) extension available for Chrome and Firefox is critical to debugging a React UI. It will let you browse the React UI much like the DOM, showing the tree of rendered components and their current props and state in real time. ## Redux -Redux is a state management tool with a tiny API that affords the developer highly predictable behaviour. All of the application state is stored in a single object, and the only way to mutate that object is by calling an action, which is just a simple object that describes what happened. A function known as a _reducer_ mutates the state based on that action and returns a new reference with the updated state. +Redux is a state management tool with a tiny API that affords the developer highly predictable behaviour. All of the application state is stored in a single object, and the only way to mutate that object is by calling an action, which is just a simple object that describes what happened. A function known as a *reducer* mutates the state based on that action and returns a new reference with the updated state. -The following example is taken from the [Redux Github page](https://github.com/reactjs/redux): +The following example is taken from the [Redux GitHub page](https://github.com/reactjs/redux): ```js // reducer function counter(state = 0, action) { switch (action.type) { - case 'INCREMENT': - return state + 1; - case 'DECREMENT': - return state - 1; - default: - return state + case 'INCREMENT': + return state + 1; + case 'DECREMENT': + return state - 1; + default: + return state; } } -let store = createStore(counter); -store.subscribe(() => - console.log(store.getState()) -); -// Call an action +const store = createStore(counter); +// subscribe to an action +store.subscribe(() => { + const state = store.g.etState(); + // ... do something with the state here +}); + +// Call an action - in this case increment the state from 0 to 1 store.dispatch({ type: 'INCREMENT' }); -// 1 ``` -### Recommended: Redux Devtools +### Recommended: redux devtools It's important to be able to view the state of the React application when you're debugging and building the interface. -To be able to view the state, you'll need to be in a dev environment +To be able to view the state, you'll need to be in a dev environment and have the [Redux Devtools](https://github.com/zalmoxisus/redux-devtools-extension) installed on Google Chrome or Firefox, which can be found by searching with your favourite search engine. - -## GraphQL and Apollo +## GraphQL and apollo [GraphQL](https://graphql.org/learn/) is a strictly-typed query language that allows you to describe what data you want to fetch from your API. Because it is based on types, it is self-documenting and predictable. Further, it's structure lends itself nicely to fetching nested objects. Here is an example of a simple GraphQL query: @@ -146,35 +152,37 @@ On its own, GraphQL offers nothing functional, as it's just a query language. Yo This documentation will stop short of explaining React, Redux, and GraphQL/Apollo in-depth, as there is much better documentation available all over the web. We recommend: -* [The Official React Tutorial](https://react.dev/learn) -* [Build With React](https://buildwithreact.com/tutorial) -* [Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux) -* [The React Apollo docs](https://www.apollographql.com/docs/react/) -* [GraphQL in Silverstripe](/developer_guides/graphql/) -## Build tools and using Silverstripe React components {#using-cms-react-components} +- [The Official React Tutorial](https://react.dev/learn) +- [Build With React](https://buildwithreact.com/tutorial) +- [Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux) +- [The React Apollo docs](https://www.apollographql.com/docs/react/) +- [GraphQL in Silverstripe](/developer_guides/graphql/) -Silverstripe CMS includes react, redux, graphql, apollo, and many other thirdparty dependencies already, which are exposed using [webpack's expose-loader plugin](https://webpack.js.org/loaders/expose-loader/) for you to use as [webpack externals](https://webpack.js.org/configuration/externals/). +## Build tools and using Silverstripe CMS react components {#using-cms-react-components} + +Silverstripe CMS includes react, redux, GraphQL, apollo, and many other thirdparty dependencies already, which are exposed using [webpack's expose-loader plugin](https://webpack.js.org/loaders/expose-loader/) for you to use as [webpack externals](https://webpack.js.org/configuration/externals/). There are also a lot of React components and other custom functionality (such as the injector, mentioned below) available for reuse. These are exposed in the same way. The recommended way to access these dependencies is by using the [@silverstripe/webpack-config npm package](https://www.npmjs.com/package/@silverstripe/webpack-config). The documentation in the readme for that package explains how to use it. -If you are not using webpack to transpile your javascript, see if your build tooling has an equivalent to webpack's `externals` configuration. Alternatively, instead of `import`ing these dependencies, you can access them on the `window` object (for example the injector module is exposed as `window.Injector`). +If you are not using webpack to transpile your JavaScript, see if your build tooling has an equivalent to webpack's `externals` configuration. Alternatively, instead of `import`ing these dependencies, you can access them on the `window` object (for example the injector module is exposed as `window.Injector`). -## The Injector API +## The `Injector` API Much like Silverstripe CMS's [Injector API](../../extending/injector) in PHP, -the client-side framework has its own implementation of dependency injection -known as `Injector`. Using Injector, you can register new services, and +the client-side framework has its own implementation of dependency injection +known as `Injector`. Using Injector, you can register new services, and transform existing services. Injector is broken up into three sub-APIs: -* `Injector.component` for React UI components -* `Injector.reducer` for Redux state management -* `Injector.form` for forms rendered via `FormSchema`. -The frontend Injector works a bit differently than its backend counterpart. Instead of _overriding_ a service with your own implementation, you _enhance_ an existing service with your own concerns. This pattern is known as [middleware](https://en.wikipedia.org/wiki/Middleware). +- `Injector.component` for React UI components +- `Injector.reducer` for Redux state management +- `Injector.form` for forms rendered via `FormSchema`. + +The frontend Injector works a bit differently than its backend counterpart. Instead of *overriding* a service with your own implementation, you *enhance* an existing service with your own concerns. This pattern is known as [middleware](https://en.wikipedia.org/wiki/Middleware). Middleware works a lot like a decorator. It doesn't alter the original API of the service, but it can augment it with new features and concerns. This has the inherent advantage of allowing all thidparty code to have an influence over the behaviour, state, and UI of a component. @@ -183,31 +191,36 @@ but it can augment it with new features and concerns. This has the inherent adva Let's say you have an application that features error logging. By default, the error logging service simply outputs to `console.error`. But you want to customise it to send errors to a thirdparty service. For this, you could use middleware to augment the default functionality of the logger. -_LoggingService.js_ ```js +// LoggingService.js + +/* eslint-disable-next-line no-console */ const LoggingService = (error) => console.error(error); export default LoggingService; ``` -Now, let's add some middleware to that service. The signature of middleware is: +Now, let's add some middleware to that service. The signature of middleware is: + ```js const middleware = (next) => (args) => next(args); ``` + Where `next()` is the next customisation in the "chain" of middleware. Before invoking the next implementation, you can add whatever customisations you need. Here's how we would use middleware to enhance `LoggingService`. ```js import thirdPartyLogger from 'third-party-logger'; const addLoggingMiddleware = (next) => (error) => { - if (error.type === LoggingService.CRITICAL) { - thirdpartyLogger.send(error.message); - } - return next(error); -} + if (error.type === LoggingService.CRITICAL) { + thirdpartyLogger.send(error.message); + } + return next(error); +}; ``` Then, we would create a new logging service that merges both implementations. + ```js import LoggingService from './LoggingService'; import addLoggingMiddleware from './addLoggingMiddleware'; @@ -218,32 +231,32 @@ const MyNewLogger = addLoggingMiddleware(LoggingService); We haven't overridden any functionality. `LoggingService(error)` will still invoke `console.error`, once all the middleware has run. But what if we did want to kill the original functionality? ```js -import LoggingService from './LoggingService'; import thirdPartyLogger from 'third-party-logger'; +import LoggingService from './LoggingService'; const addLoggingMiddleware = (next) => (error) => { - // Critical errors go to a thirdparty service - if (error.type === LoggingService.CRITICAL) { - thirdPartyLogger.send(error.message); - } - // Other errors get logged, but not to our thirdparty - else if (error.type === LoggingService.ERROR) { - next(error); - } - // Minor errors are ignored - else { - // Do nothing! - } -} + // Critical errors go to a thirdparty service + if (error.type === LoggingService.CRITICAL) { + thirdPartyLogger.send(error.message); + } + // Other errors get logged, but not to our thirdparty + else if (error.type === LoggingService.ERROR) { + next(error); + } + // Minor errors are ignored + else { + // Do nothing! + } +}; ``` -### Registering new services to the Injector +### Registering new services to the `Injector` -If you've created a module using React, it's a good idea to afford other developers an +If you've created a module using React, it's a good idea to afford other developers an API to enhance those components, forms, and state. To do that, simply register them with `Injector`. -__my-public-module/js/main.js__ ```js +// my-public-module/js/main.js import Injector from 'lib/Injector'; Injector.component.register('MyComponent', MyComponent); @@ -260,94 +273,86 @@ const MyComponent = Injector.component.get('MyComponent'); Because of the unique structure of the `form` middleware, you cannot register new services to `Injector.form`. [/notice] - [alert] Overwriting components by calling `register()` multiple times for the same service name is discouraged, and will throw an error. Should you really need to do this, you can pass `{ force: true }` as the third argument to the `register()` function. [/alert] - ### Transforming services using middleware Now that the services are registered, other developers can customise your services with `Injector.transform()`. -__someone-elses-module/js/main.js__ - ```js +// someone-elses-module/js/main.js Injector.transform( - 'my-transformation', - (updater) => { - updater.component('MyComponent', MyCustomComponent); - updater.reducer('myCustom', MyCustomReducer); - - } + 'my-transformation', + (updater) => { + updater.component('MyComponent', MyCustomComponent); + updater.reducer('myCustom', MyCustomReducer); + } ); - ``` Much like the configuration layer, we need to specify a name for this transformation. This will help other modules negotiate their priority over the injector in relation to yours. -The second parameter of the `transform` argument is a callback which receives an `updater`object. It contains four functions: `component()`, `reducer()`, `form.alterSchema()` and `form.addValidation()`. We'll cover all of these in detail functions in detail further into the document, but briefly, these update functions allow you to mutate the DI container with a wrapper for the service. Remember, this function does not _replace_ +The second parameter of the `transform` argument is a callback which receives an `updater`object. It contains four functions: `component()`, `reducer()`, `form.alterSchema()` and `form.addValidation()`. We'll cover all of these in detail functions in detail further into the document, but briefly, these update functions allow you to mutate the DI container with a wrapper for the service. Remember, this function does not *replace* the service - it enhances it with new functionality. -#### Helpful tip: Name your component middleware +#### Helpful tip: name your component middleware Since multiple enhancements can be applied to the same component, it will be really useful for debugging purposes to reveal the names of each enhancement on the `displayName` of - the component. This will really help you when viewing the rendered component tree in - [React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en). - + the component. This will really help you when viewing the rendered component tree in + [React Dev Tools](https://chrome.g.oogle.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en). + For this, you can use the third parameter of the `updater.component` function. It takes an arbitrary name for the enhancement you're applying. - - __module-a/js/main.js__ + ```js - (updater) => updater.component('TextField', CharacterCounter, 'CharacterCounter') + // module-a/js/main.js + (updater) => updater.component('TextField', CharacterCounter, 'CharacterCounter'); ``` - __module-b/js/main.js__ + ```js - (updater) => updater.component('TextField', TextLengthChecker, 'TextLengthChecker') + // module-b/js/main.js + (updater) => updater.component('TextField', TextLengthChecker, 'TextLengthChecker'); ``` - ### Controlling the order of transformations Sometimes, it's critical to ensure that your customisation happens after another one has been executed. To afford you control over the ordering of transforms, Injector allows `before` and `after` attributes as metadata for the transformation. -__my-module/js/main.js__ - ```js +// my-module/js/main.js Injector.transform( - 'my-transformation', - (updater) => { - updater.component('MyComponent', MyCustomComponent); - updater.reducer('myCustom', MyCustomReducer); - - }, - { after: 'another-module' } + 'my-transformation', + (updater) => { + updater.component('MyComponent', MyCustomComponent); + updater.reducer('myCustom', MyCustomReducer); + }, + { after: 'another-module' } ); - ``` `before` and `after` also accept arrays of constraints. ```js Injector.transform( - 'my-transformation', - (updater) => updater.component('MyComponent', MyCustomComponent); + 'my-transformation', + (updater) => updater.component('MyComponent', MyCustomComponent), { before: ['my-transformation', 'some-other-transformation'] } ); ``` -#### Using the * flag +#### Using the `*` flag -If you really want to be sure your customisation gets loaded first or last, you can use -`*` as your `before` or `after` reference. +If you really want to be sure your customisation gets loaded first or last, you can use +`*` as your `before` or `after` reference. ```js Injector.transform( - 'my-transformation', + 'my-transformation', (updater) => updater.component('MyComponent', FinalTransform), { after: '*' } ); @@ -356,8 +361,9 @@ Injector.transform( [info] This flag can only be used once per transformation. The following are not allowed: -* `{ before: ['*', 'something-else'] }` -* `{ after: '*', before: 'something-else' }` + +- `{ before: ['*', 'something-else'] }` +- `{ after: '*', before: 'something-else' }` [/info] @@ -365,7 +371,8 @@ The following are not allowed: Because so much of UI design depends on context, dependency injection in the frontend is not necessarily universal. Instead, services are fetched with context. -_example_: +*example:* + ```js const CalendarComponent = Injector.get('Calendar', 'AssetAdmin.FileEditForm.StartDate'); ``` @@ -374,38 +381,40 @@ Likewise, services can be applied for specific contexts. ```js Injector.transform('my-transform', (updater) => { - // Applies to all text fields in AssetAdmin - updater.component('TextField.AssetAdmin', MyTextField); + // Applies to all text fields in AssetAdmin + updater.component('TextField.AssetAdmin', MyTextField); - // Applies to all text fields in AssetAdmin editform - updater.component('TextField.AssetAdmin.FileEditForm', MyTextField); + // Applies to all text fields in AssetAdmin editform + updater.component('TextField.AssetAdmin.FileEditForm', MyTextField); - // Applies to any textfield named "Title" in AssetAdmin - updater.component('TextField.AssetAdmin.*.Title', MyTextField); + // Applies to any textfield named "Title" in AssetAdmin + updater.component('TextField.AssetAdmin.*.Title', MyTextField); - // Applies to any textfield named "Title" in any admin - updater.component('TextField.*.*.Title', MyTextField); -}) + // Applies to any textfield named "Title" in any admin + updater.component('TextField.*.*.Title', MyTextField); +}); ``` - To apply context-based transformations, you'll need to know the context of the component you want to customise. To learn this, - open your React Developer Tools (see above) window and inspect the component name. The - context of the component is displayed between two square brackets, appended to the original name, for example: - `TextField[TextField.AssetAdmin.FileEditForm.Title]`. The context description is hierarchical, starting - with the most general category (in this case, "Admin") and working its way down to the most specific - category (Name = 'Title'). You can use Injector to hook into the level of specificity that you want. - +To apply context-based transformations, you'll need to know the context of the component you want to customise. To learn this, +open your React Developer Tools (see above) window and inspect the component name. The +context of the component is displayed between two square brackets, appended to the original name, for example: +`TextField[TextField.AssetAdmin.FileEditForm.Title]`. The context description is hierarchical, starting +with the most general category (in this case, "Admin") and working its way down to the most specific +category (Name = 'Title'). You can use Injector to hook into the level of specificity that you want. -## Customising React components with Injector +## Customising react components with `Injector` When middleware is used to customise a React component, it is known as a [higher order component](https://facebook.github.io/react/docs/higher-order-components.html). Using the `PhotoItem` example above, let's create a customised `PhotoItem` that allows a badge, perhaps indicating that it is new to the gallery. ```js +import React from 'react'; +// ... + const enhancedPhoto = (PhotoItem) => (props) => { - const badge = props.isNew ? -
                New!
                : + const badge = props.isNew ? +
                New!
                : null; return ( @@ -414,22 +423,25 @@ const enhancedPhoto = (PhotoItem) => (props) => {
                ); -} +}; const EnhancedPhotoItem = enhancedPhoto(PhotoItem); - + ; ``` Alternatively, this component could be expressed with an ES6 class, rather than a simple function. ```js +import React from 'react'; +// ... + const enhancedPhoto = (PhotoItem) => ( class EnhancedPhotoItem extends React.Component { render() { - const badge = this.props.isNew ? -
                New!
                : + const badge = this.props.isNew ? +
                New!
                : null; return ( @@ -445,35 +457,36 @@ const enhancedPhoto = (PhotoItem) => ( When components are stateless, using a simple function in lieu of a class is recommended. - -## Using dependencies within your React component +## Using dependencies within your react component If your component has dependencies, you can add them via the injector using the `inject()` higher order component. The function accepts the following arguments: ```js -inject([dependencies], mapDependenciesToProps, getContext)(Component) +inject([dependencies], mapDependenciesToProps, getContext)(Component); ``` -* **[dependencies]**: An array of dependencies (or a string, if just one) -* **mapDependenciesToProps**: (optional) All dependencies are passed into this function as params. The function + +- **[dependencies]**: An array of dependencies (or a string, if just one) +- **mapDependenciesToProps**: (optional) All dependencies are passed into this function as params. The function is expected to return a map of props to dependencies. If this parameter is not specified, the prop names and the service names will mirror each other. -* **getContext**: A callback function with params `(props, currentContext)` that will calculate the context to +- **getContext**: A callback function with params `(props, currentContext)` that will calculate the context to use for determining which transformations apply to the dependencies. This defaults to the current context. This could help when any customisations that may calls for a change (or tweak) to the current context. The result is a function that is ready to apply to a component. - + ```js const MyInjectedComponent = inject( - ['Dependency1', 'Dependency2'] + ['Dependency1', 'Dependency2'] )(MyComponent); // MyComponent now has access to props.Dependency1 and props.Dependency2 ``` + Here is its usage with a bit more context: -__my-module/js/components/Gallery.js__ ```js +// my-module/js/components/Gallery.js import React from 'react'; import { inject } from 'lib/Injector'; @@ -481,8 +494,8 @@ class Gallery extends React.Component { render() { const { SearchComponent, ItemComponent } = this.props; return ( -
                - +
                + {this.props.items.map(item => ( ))} @@ -491,8 +504,8 @@ class Gallery extends React.Component { } } -export default inject( - ['GalleryItem', 'SearchBar'], +export default inject( + ['GalleryItem', 'SearchBar'], (GalleryItem, SearchBar) => ({ ItemComponent: GalleryItem, SearchComponent: SearchBar @@ -506,8 +519,8 @@ The properties used by `inject()` are soft-supplied. This means a parent calling Here is an example using the above `Gallery` component with the dependency `ItemComponent` overwritten by the calling component. We pull in a previously registered `PreviewItem` to replace the former `GalleryItem`. -__my-module/js/components/PreviewSection.js__ ```js +// my-module/js/components/PreviewSection.js import React from 'react'; import { inject } from 'lib/Injector'; @@ -531,8 +544,8 @@ export default inject( Another way to provide context to injector is by using the `provideContext` HOC, rather than the `getContext` param in `inject()`. -__my-module/js/components/ContextualSection.js__ ```js +// my-module/js/components/ContextualSection.js import React, { Component } from 'react'; import { provideContext, inject } from 'lib/Injector'; @@ -554,15 +567,17 @@ declare them in `inject()`. In cases like this, use `withInjector()`. This highe component puts the `Injector` instance in `context`. ```js -class MyGallery extends React.Component -{ - render () { -
                +import React from 'react'; +// ... + +class MyGallery extends React.Component { + render() { + return
                {this.props.items.map(item => { const Component = this.context.injector.get(item.type, 'Reports.context'); - return + return ; })} -
                +
                ; } } @@ -573,8 +588,7 @@ The `Reports.context` in the second parameter provides a context for the injecto which transformations to apply to or remove from the component you're looking to get. More details about transformations below. - -## Using Injector to customise forms +## Using injector to customise forms Forms in the React layer are built declaratively, using the `FormSchema` API. A component called `FormBuilderLoader` is given a URL to a form schema definition, and it populates itself with fields @@ -589,17 +603,17 @@ we'll use the `updater.form.alterSchema()` function. ```js Injector.transform( - 'my-custom-form', - (updater) => { - updater.form.alterSchema( - 'AssetAdmin.*', - (form) => + 'my-custom-form', + (updater) => { + updater.form.alterSchema( + 'AssetAdmin.*', + (form) => form.updateField('Title', { - myCustomProp: true + myCustomProp: true }) - .getState() - ) - } + .getState() + ); + } ); ``` @@ -607,17 +621,17 @@ The `alterSchema()` function takes a callback, with an instance of `FormStateMan above example) as a parameter. `FormStateMangaer` allows you to declaratively update the form schema API using several helper methods, including: -* `updateField(fieldName:string, updates:object)` -* `updateFields({ myFieldName: updates:object })` -* `mutateField(fieldName:string, callback:function)` -* `setFieldComponent(fieldName:string, componentName:string)` -* `setFieldClass(fieldName:string, cssClassName:string, active:boolean)` -* `addFieldClass(fieldName:string, cssClassName:string)` -* `removeFieldClass(fieldName:string, cssClassName:string)` +- `updateField(fieldName:string, updates:object)` +- `updateFields({ myFieldName: updates:object })` +- `mutateField(fieldName:string, callback:function)` +- `setFieldComponent(fieldName:string, componentName:string)` +- `setFieldClass(fieldName:string, cssClassName:string, active:boolean)` +- `addFieldClass(fieldName:string, cssClassName:string)` +- `removeFieldClass(fieldName:string, cssClassName:string)` [info] For a complete list of props that are available to update on a `Field` object, -see https://redux-form.com/8.3.0/docs/api/field.md/#props-you-can-pass-to-field- +see [/info] [notice] @@ -626,12 +640,12 @@ It is critical that you end series of mutation calls with `getState()`. In addition to mutation methods, several readonly methods are available on `FormSchemaManager` to read the current form state, including: -* `getValues()`: Returns a map of field names to their current values -* `getValue(fieldName:string)`: Returns the value of the given field -* `isDirty()`: Returns true if the form has been mutated from its original state -* `isPristine()`: Returns true if the form is in its original state -* `isValid()`: Returns true if the form has no validation errors -* `isInvalid()`: Returns true if the form has validation errors +- `getValues()`: Returns a map of field names to their current values +- `getValue(fieldName:string)`: Returns the value of the given field +- `isDirty()`: Returns true if the form has been mutated from its original state +- `isPristine()`: Returns true if the form is in its original state +- `isValid()`: Returns true if the form has no validation errors +- `isInvalid()`: Returns true if the form has validation errors ### Adding validation to a form @@ -648,36 +662,38 @@ Injector.transform( validator.addError('PostalCode', 'Invalid postal code'); } } - ) + ); } ); ``` The `addValidation()` function takes a callback, with an instance of `FormValidationManager` (`validator` in the above example) as a parameter. `FormValidationMangaer` allows you to manage the validation result using several helper methods, including: -* `addError(fieldName:string, message:string)` -* `addErrors(fieldName:string, messages:Array)` -* `hasError(fieldName:string)` -* `clearErrors(fieldName:string)` -* `getErrors(fieldName:string)` -* `reset(void)` - +- `addError(fieldName:string, message:string)` +- `addErrors(fieldName:string, messages:Array)` +- `hasError(fieldName:string)` +- `clearErrors(fieldName:string)` +- `getErrors(fieldName:string)` +- `reset(void)` -## Using Injector to customise Redux state data +## Using injector to customise redux state data Before starting this tutorial, you should become familiar with the concepts of [Immutability](https://www.sitepoint.com/immutability-javascript/) and [Redux](https://redux.js.org). For example: + ```js - newProps = { ...oldProps, name: 'New name' }; +newProps = { ...oldProps, name: 'New name' }; ``` + is the same as + ```js - newProps = Object.assign( - {}, - oldProps, - { name: 'New name' } - ); +newProps = Object.assign( + {}, + oldProps, + { name: 'New name' } +); ``` To start customising, you'll need to transform an existing registered reducer, you can find what reducers are registered by importing Injector and running `Injector.reducer.getAll()` @@ -690,7 +706,7 @@ Injector.transform('customisationName', (updater) => { As you can see, we use the `reducer()` function on the `update` object to augment Redux state transformations. -### Using Redux dev tools +### Using redux dev tools It is important to learn the basics of [Redux dev tools](https://github.com/reduxjs/redux-devtools/tree/main/extension#installation), so that you can find out what ACTIONS and payloads to intercept and modify in your Transformer should target. @@ -699,6 +715,7 @@ Most importantly, it helps to understand the "Action" sub-tab on the right panel ### Structuring a transformer We use currying to supply utilities which your transformer may require to handle the transformation. + - `originalReducer` - reducer callback which the transformer is customising, this will need to be called in most cases. This will also callback other transformations down the chain of execution. Not calling this will break the chain. - `getGlobalState` - A function that gets the state of the global Redux store. There may be data outside the current scope in the reducer which you may need to help determine the transformation. - `state` - current state of the current scope. This is what should be used to form the new state. @@ -710,11 +727,12 @@ const MyReducerTransformer = (originalReducer) => (globalState) => (state, { typ switch (type) { case 'EXISTING_ACTION': { // recommended to call and return the originalReducer with the payload changed by the transformer - /* return action to call here; */ + return originalReducer(/* ... */); } - + case 'OVERRIDE_EXISTING_ACTION': { // could omit the originalReducer to enforce your change or cancel the originalREducer's change + return originalReducer(/* ... */); } default: { @@ -722,7 +740,7 @@ const MyReducerTransformer = (originalReducer) => (globalState) => (state, { typ return originalReducer(state, { type, payload }); } } -} +}; ``` ### A basic transformation @@ -746,6 +764,9 @@ const MyReducerTransformer = (originalReducer) => (getGlobalState) => (state, { }, }); } + default: { + return state; + } } }; ``` @@ -757,11 +778,14 @@ Accessing the globalState is easy, as it is passed in as part of the curried fun ```js export default (originalReducer) => (getGlobalState) => (state, { type, payload }) => { const baseUrl = globalState.config.baseUrl; - + switch (type) { /* ... cases here ... */ + default: { + // ... + } } -} +}; ``` ### Setting a different initial state @@ -777,6 +801,7 @@ const MyReducerTransformer = (originalReducer) => () => (state, { type, payload myCustom: 'initial state here', }; } + return state; }; ``` @@ -791,6 +816,9 @@ export default (originalReducer) => (getGlobalState) => (state, { type, payload case 'CANCEL_THIS_ACTION': { return state; } + default: { + return state; + } } }; ``` @@ -801,7 +829,7 @@ You could manipulate the action called by the originalReducer, there isn't an ex code will present the theory of how it can be achieved. ```js - default (originalReducer) => (getGlobalState) => (state, { type, payload }) => { +export default (originalReducer) => (getGlobalState) => (state, { type, payload }) => { switch (type) { case 'REMOVE_ERROR': { // we'd like to archive errors instead of removing them @@ -810,11 +838,14 @@ You could manipulate the action called by the originalReducer, there isn't an ex payload, }); } + default: { + return state; + } } }; ``` -## Using Injector to customise GraphQL queries +## Using injector to customise GraphQL queries One of the strengths of GraphQL is that it allows us to declaratively state exactly what data a given component needs to function. Because GraphQL queries and mutations are considered primary concerns of a component, they are not abstracted away somewhere in peripheral asynchronous functions. Rather, they are co-located with the component definition itself. @@ -832,9 +863,8 @@ Let's imagine that we have a module that adds a tab where the user can write "no Here's what that might look like: -**my-module/client/src/components/Notes.js** - ```js +// my-module/client/src/components/Notes.js import React from 'react'; import gql from 'graphql-tag'; import { graphql } from '@apollo/client/react/hoc'; @@ -857,7 +887,7 @@ query ReadNotes { const apolloConfig = { props({ data: { readNotes } }) { return { - notes: readNotes ? readNotes : [] + notes: readNotes || [] }; } }; @@ -869,9 +899,9 @@ export default NotesWithData; Next we'll expose the model to GraphQL: -**my-module/_config/graphql.yml** - ```yml +# my-module/_config/graphql.yml + # Tell graphql that we're adding to the admin graphql schema SilverStripe\GraphQL\Schema\Schema: schemas: @@ -880,9 +910,9 @@ SilverStripe\GraphQL\Schema\Schema: - my-module/_graphql ``` -**my-module/_graphql/models.yml** - ```yml +# my-module/_graphql/models.yml + # Tell graphql how to scaffold the schema for our model App\Model\Note: fields: @@ -898,9 +928,8 @@ App\Model\Note: Finally, let's make a really simple container app which holds a header and our notes component, and inject it into the DOM using entwine. -**my-module/client/src/App.js** - ```js +// my-module/client/src/App.js import React from 'react'; import Notes from './components/Notes'; @@ -914,7 +943,6 @@ const App = () => ( export default App; ``` -**my-module/client/src/index.js** ```js import { createRoot } from 'react-dom/client'; import React from 'react'; @@ -936,10 +964,10 @@ Injector.ready(() => { - ) + ); }, - onunmatch: function() { + onunmatch() { const root = this.getReactRoot(); if (root) { root.unmount(); @@ -962,15 +990,14 @@ To mount the app, we use the `onmatch()` event fired by entwine, and we're off a What we've just built may work, but we've made life very difficult for other developers. They have no way of customising this. Let's change that. -#### Register as much as possible with Injector +#### Register as much as possible with injector The best thing you can do to make your code extensible is to use `Injector` early and often. Anything that goes through Injector is easily customisable. First, let's break up the list into smaller components. -**my-module/client/src/components/NotesList.js** - ```js +// my-module/client/src/components/NotesList.js import React from 'react'; import { inject } from 'lib/Injector'; @@ -994,9 +1021,8 @@ export default inject( )(NotesList); ``` -**my-module/client/src/components/NotesListItem.js** - ```js +// my-module/client/src/components/NotesListItem.js import React from 'react'; const NotesListItem = ({ note }) =>
              • {note.content}
              • ; @@ -1008,9 +1034,8 @@ export default NotesListItem; The next piece is the query. We'll need to register that with `Injector`. Unlike components and reducers, this is a lot more abstract. We're actually not going to write any GraphQL at all. We'll just build the concept of the query in an abstraction layer, and leave `Injector` to build the GraphQL syntax at runtime. -**my-module/client/src/state/readNotes.js** - ```js +// my-module/client/src/state/readNotes.js import { graphqlTemplates } from 'lib/Injector'; const { READ } = graphqlTemplates; @@ -1019,8 +1044,8 @@ const query = { apolloConfig: { props({ data: { readNotes } }) { return { - notes: readNotes ? readNotes : [], - } + notes: readNotes || [], + }; } }, templateName: READ, @@ -1045,10 +1070,10 @@ For simplicity, we're not querying any relations or otherwise nested data here. ```js const query = { - //... + // ... fields: [ 'foo', [ - 'title', + 'title', ] ], }; @@ -1061,13 +1086,12 @@ You might instinctively try to use JSON object notation for this instead, but th Let's now register all of this with Injector. -**my-module/client/src/boot/registerDependencies.js** - ```js +// my-module/client/src/boot/registerDependencies.js +import Injector, { injectGraphql } from 'lib/Injector'; import NotesList from '../components/NotesList'; import NotesListItem from '../components/NotesListItem'; import readNotes from '../state/readNotes'; -import Injector, { injectGraphql } from 'lib/Injector'; const registerDependencies = () => { Injector.component.register('NotesList', NotesList); @@ -1083,9 +1107,9 @@ If you have a lot of components or queries to add, you can use `registerMany` in ```js Injector.component.registerMany({ - NotesList, - NotesListItem, - //...etc + NotesList, + NotesListItem, + // ...etc }); ``` @@ -1097,9 +1121,9 @@ We use `Injector.query.register()` to register our `readNotes` query so that oth The only missing piece now is to attach the `ReadNotes` injected query to the `NotesList` component. We could have done this using `injectGraphql` in the `NotesList` component itself, but instead, we'll do it as an Injector transformation. Why? There's a good chance whoever is customising the query will want to customise the UI of the component that is using that query. If someone adds a new field to a query, it is likely the component should display that new field. Registering the GraphQL injection as a transformation will allow a thirdparty developer to override the UI of the component explicitly *after* the GraphQL query is attached. This is important, because otherwise the customised component wouldn't use the query. -**my-module/client/src/boot/registerDependencies.js** - ```js +// my-module/client/src/boot/registerDependencies.js + // ... const registerDependencies = () => { // ... @@ -1122,9 +1146,8 @@ All of this feels like a lot of extra work, and, to be fair, it is. You're proba Our container app needs to have the `NotesList` component injected into it. -**my-module/client/src/App.js** - ```js +// my-module/client/src/App.js import React from 'react'; import { inject } from 'lib/Injector'; @@ -1140,18 +1163,16 @@ export default inject(['NotesList'])(App); You can register the `App` component with `Injector`, too, but since it's already injected with dependencies it could get pretty convoluted. High level components like this are best left uncustomisable. -#### Use the Injector from an entwine context +#### Use the injector from an entwine context Since almost everything is in `Injector` now, we need to update our mounting logic to inject the dependencies into our app. -**my-module/client/src/index.js** - ```js import { createRoot } from 'react-dom/client'; import React from 'react'; -import registerDependencies from './boot/registerDependencies'; import { ApolloProvider } from '@apollo/client'; import Injector, { provideInjector } from 'lib/Injector'; +import registerDependencies from './boot/registerDependencies'; import App from './App'; registerDependencies(); @@ -1174,7 +1195,7 @@ Injector.ready(() => { root.render(); }, - onunmatch: function() { + onunmatch() { const root = this.getReactRoot(); if (root) { root.unmount(); @@ -1197,22 +1218,20 @@ Let's suppose we have a project that extends the `Notes` object in some way. Per We'll first need to apply the extension and update our GraphQL scaffolding. -**app/_config/extensions.yml** - ```yml +# app/_config/extensions.yml App\Model\Note: extensions: # this extension adds a "Priority" integer field - MyOtherApp\Extension\NoteExtension ``` -Remember, this example is in a project which is customising the schema from the previous example, so we still have to tell graphql where to find our schema modifications. +Remember, this example is in a project which is customising the schema from the previous example, so we still have to tell GraphQL where to find our schema modifications. If you're following along, you could declare a different folder than before within the same project so you can see how the schema definitions merge together into a single schema. -**app/_config/graphql.yml** - ```yml +# app/_config/graphql.yml SilverStripe\GraphQL\Schema\Schema: schemas: admin: @@ -1220,9 +1239,8 @@ SilverStripe\GraphQL\Schema\Schema: - app/_graphql ``` -**app/_graphql/models.yml** - ```yml +# app/_graphql/models.yml App\Model\Note: fields: priority: true @@ -1232,13 +1250,12 @@ App\Model\Note: Let's first update the `NotesListItem` to contain our new field. -**app/client/src/transformNotesListItem.js** - [notice] Note that we're overriding the entire `NotesListItem` component. This is the main reason we broke the original list up into smaller components. [/notice] ```js +// app/client/src/transformNotesListItem.js import React from 'react'; const transformNotesListItem = () => ({ note: { content, priority } }) => ( @@ -1250,9 +1267,8 @@ export default transformNotesListItem; Now, let's update the query to fetch our new field. -**app/client/src/transformReadNotes.js** - ```js +// app/client/src/transformReadNotes.js const transformReadNotes = (manager) => { manager.addField('priority'); }; @@ -1267,7 +1283,7 @@ Simple! The transformation passes us a `ApolloGraphQLManager` instance that prov In the above example, we added a single field to a query. Here's how that works: ```js -manager.addField(fieldName, fieldPath = 'root') +manager.addField(fieldName, fieldPath = 'root'); ``` The `fieldPath` argument tells the manager at what level to add the field. In this case, since the `priority` field is going on the root query (`readNotes`), we'll use `root` as the path. But suppose we had a more complex query like this: @@ -1319,9 +1335,8 @@ Where `root/company/logo` is the path to the field, `size` is the name of the ar Now, let's apply all these transformations, and we'll use the `after` property to ensure they get applied in the correct sequence. -**app/client/src/boot.js** - ```js +// app/client/src/boot.js import Injector from 'lib/Injector'; import transformNotesListItem from './transformNotesListItem'; import transformReadNotes from './transformReadNotes'; @@ -1337,7 +1352,7 @@ Injector.transform( ``` [hint] -This transformation could either be transpiled as-is, or if you have other javascript to include in this module you might want to export it as a function and call it from some entry point. +This transformation could either be transpiled as-is, or if you have other JavaScript to include in this module you might want to export it as a function and call it from some entry point. Don't forget to add the transpiled result to the CMS e.g. via the `SilverStripe\Admin\LeftAndMain.extra_requirements_javascript` configuration property. [/hint] @@ -1345,21 +1360,21 @@ Don't forget to add the transpiled result to the CMS e.g. via the `SilverStripe\ Going back to the original module, let's add an `AddForm` component to our list that lets the user create a new note. -**my-module/client/src/components/AddForm.js** - ```js -import React from 'react'; +// my-module/client/src/components/AddForm.js +import React, { useRef } from 'react'; const AddForm = ({ onAdd }) => { - let input; + const inputRef = useRef(null); return (
                - input = node}/> + + onAdd(inputRef && inputRef.value); + }} + >Add
                ); }; @@ -1373,9 +1388,8 @@ Because this isn't a full react tutorial, we've avoided the complexity of ensuri And we'll inject that component into our `App` container. -**my-module/client/src/App.js** - ```js +// my-module/client/src/App.js import React from 'react'; import { inject } from 'lib/Injector'; @@ -1392,9 +1406,8 @@ export default inject(['NotesList', 'NoteAddForm'])(App); Next, add a mutation template to attach to the form. -**my-module/client/src/state/createNote.js** - ```js +// my-module/client/src/state/createNote.js import { graphqlTemplates } from 'lib/Injector'; const { CREATE } = graphqlTemplates; @@ -1411,7 +1424,7 @@ const mutation = { } }); } - } + }; } }, templateName: CREATE, @@ -1428,11 +1441,10 @@ export default mutation; It looks like a lot of code, but if you're familiar with Apollo mutations, this is pretty standard. The supplied `mutate()` function gets mapped to a prop - in this case `onAdd`, which the `AddForm` component is configured to invoke. We've also supplied the `singularName` as well as the template `CREATE` for the `createNote` scaffolded mutation. -And make sure we're exposing the mutation in our graphql schema: - -**my-module/_graphql/models.yml** +And make sure we're exposing the mutation in our GraphQL schema: ```yml +# my-module/_graphql/models.yml App\Model\Note: #... operations: @@ -1442,19 +1454,18 @@ App\Model\Note: Lastly, let's just register all this with `Injector`. -**my-module/client/src/boot/registerDependencies.js** - ```js -//... +// my-module/client/src/boot/registerDependencies.js import AddForm from '../components/AddForm'; import createNote from '../state/createNote'; +// ... const registerDependencies = () => { - //... + // ... Injector.component.register('NoteAddForm', AddForm); Injector.query.register('CreateNote', createNote); - //... + // ... Injector.transform( 'notesaddform-graphql', (updater) => { @@ -1472,28 +1483,30 @@ This is exactly the same pattern as we did before with a query, only with differ Now let's switch back to the project where we're customising the Notes application. The developer is going to want to ensure that users can supply a "Priority" value for each note entered. This will involve updating the `AddForm` component. -**app/client/src/transformAddForm.js** ```js -import React from 'react'; +// app/client/src/transformAddForm.js +import React, { useRef } from 'react'; const transformAddForm = () => ({ onAdd }) => { - let content, priority; + const contentRef = useRef(null); + const priorityRef = useRef(null); return (
                - content = node}/> + - + }} + >Add
                ); }; @@ -1503,9 +1516,8 @@ export default transformAddForm; We're now passing two arguments to the `onAdd` callback - one for the note content, and another for the priority. We'll need to update the mutation to reflect this. -**app/client/src/transformCreateNote.js** - ```js +// app/client/src/transformCreateNote.js const transformCreateNote = (manager) => { manager.addField('priority'); manager.transformApolloConfig('props', ({ mutate }) => (prevProps) => { @@ -1525,7 +1537,7 @@ const transformCreateNote = (manager) => { ...prevProps, onAdd, }; - }) + }); }; export default transformCreateNote; @@ -1535,17 +1547,16 @@ All we've done here is overridden the `props` setting in the `CreateNote` apollo Now we just need to register these transforms, and we're done! -**app/client/src/boot.js** - ```js -//... +// app/client/src/boot.js import transformAddForm from './transformAddForm'; import transformCreateNote from './transformCreateNote'; +// ... Injector.transform( 'noteslist-query-extension', (updater) => { - //... + // ... updater.component('NoteAddForm', transformAddForm); updater.query('CreateNote', transformCreateNote); }, diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md index b006c816b..9bc3ebf60 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md @@ -16,8 +16,8 @@ frontend. This how-to will walk you through creation of a "Clean-up" button with two appearances: -* active: "Clean-up now" green constructive button if the actions can be performed -* neutral: "Cleaned" default button if the action does not need to be done +- active: "Clean-up now" green constructive button if the actions can be performed +- neutral: "Cleaned" default button if the action does not need to be done The controller code that goes with this example is listed in [Extend CMS Interface](extend_cms_interface). @@ -26,27 +26,36 @@ The controller code that goes with this example is listed in [Extend CMS Interfa First create and configure the action button with alternate state on a page type. The button comes with the default state already, so you just need to add the alternate state using two data additional attributes: -* `data-icon-alternate`: icon to be shown when the button is in the alternate state -* `data-text-alternate`: likewise for text. +- `data-icon-alternate`: icon to be shown when the button is in the alternate state +- `data-text-alternate`: likewise for text. Here is the configuration code for the button: - ```php -public function getCMSActions() +namespace App\Model; + +use SilverStripe\Forms\FormAction; +use SilverStripe\ORM\DataObject; + +class MyObject extends DataObject { - $fields = parent::getCMSActions(); - - $fields->fieldByName('MajorActions')->push( - $cleanupAction = FormAction::create('cleanup', 'Cleaned') - // Set up an icon for the neutral state that will use the default text. - ->setAttribute('data-icon', 'accept') - // Initialise the alternate constructive state. - ->setAttribute('data-icon-alternate', 'addpage') - ->setAttribute('data-text-alternate', 'Clean-up now') - ); - - return $fields; + // ... + + public function getCMSActions() + { + $fields = parent::getCMSActions(); + + $fields->fieldByName('MajorActions')->push( + $cleanupAction = FormAction::create('cleanup', 'Cleaned') + // Set up an icon for the neutral state that will use the default text. + ->setAttribute('data-icon', 'accept') + // Initialise the alternate constructive state. + ->setAttribute('data-icon-alternate', 'addpage') + ->setAttribute('data-text-alternate', 'Clean-up now') + ); + + return $fields; + } } ``` @@ -58,16 +67,24 @@ used for initialisation though. Here we initialise the button based on the backend check, and assume that the button will only update after page reload (or on CMS action). - ```php -public function getCMSActions() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class MyObject extends DataObject { // ... - if ($this->needsCleaning()) { - // Will initialise the button into alternate state. - $cleanupAction->addExtraClass('ss-ui-alternate'); + + public function getCMSActions() + { + // ... + if ($this->needsCleaning()) { + // Will initialise the button into alternate state. + $cleanupAction->addExtraClass('ss-ui-alternate'); + } + // ... } - // ... } ``` @@ -84,47 +101,43 @@ frontend. You can affect the state of the button through the jQuery UI calls. First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works. - ```js jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('toggleAlternate'); ``` Another, more useful, scenario is to check the current state. - ```js jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('option', 'showingAlternate'); ``` You can also force the button into a specific state by using UI options. - ```js -jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button({showingAlternate: true}); +jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button({ showingAlternate: true }); ``` This will allow you to react to user actions in the CMS and give immediate feedback. Here is an example taken from the CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons (changetracker will automatically add `changed` class to the form if a modification is detected). - ```js /** * Enable save buttons upon detecting changes to content. * "changed" class is added by jQuery.changetracker. */ $('.cms-edit-form .changed').entwine({ - // This will execute when the class is added to the element. - onmatch: function(e) { - var form = this.closest('.cms-edit-form'); - form.find('#Form_EditForm_action_save').button({showingAlternate: true}); - form.find('#Form_EditForm_action_publish').button({showingAlternate: true}); - this._super(e); - }, - // Entwine requires us to define this, even if we don't use it. - onunmatch: function(e) { - this._super(e); - } + // This will execute when the class is added to the element. + onmatch(e) { + const form = this.closest('.cms-edit-form'); + form.find('#Form_EditForm_action_save').button({ showingAlternate: true }); + form.find('#Form_EditForm_action_publish').button({ showingAlternate: true }); + this._super(e); + }, + // Entwine requires us to define this, even if we don't use it. + onunmatch(e) { + this._super(e); + } }); ``` @@ -133,10 +146,10 @@ $('.cms-edit-form .changed').entwine({ `ssui.button` defines several additional events so that you can extend the code with your own behaviours. For example this is used in the CMS to style the buttons. Three events are available: -* `ontogglealternate`: invoked when the `toggleAlternate` is called. Return `false` to prevent the toggling. -* `beforerefreshalternate`: invoked before the alternate-specific rendering takes place, including the button +- `ontogglealternate`: invoked when the `toggleAlternate` is called. Return `false` to prevent the toggling. +- `beforerefreshalternate`: invoked before the alternate-specific rendering takes place, including the button initialisation. -* `afterrefreshalternate`: invoked after the rendering has been done, including on init. Good place to add styling +- `afterrefreshalternate`: invoked after the rendering has been done, including on init. Good place to add styling extras. Continuing our example let's add a "constructive" style to our *Clean-up* button. First you need to be able to add @@ -154,35 +167,29 @@ You can now add the styling in response to `afterrefreshalternate` event. Let's leaks. The only complex part here is how the entwine handle is constructed. `onbuttonafterrefreshalternate` can be disassembled into: -* `on` signifies the entiwne event handler -* `button` is jQuery UI widget name -* `afterrefreshalternate`: the event from ssui.button to react to. +- `on` signifies the entiwne event handler +- `button` is jQuery UI widget name +- `afterrefreshalternate`: the event from ssui.button to react to. Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all cases. - ```js -(function($) { - - $.entwine('mysite', function($){ - $('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').entwine({ - /** +jQuery.entwine('mysite', ($) => { + $('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').entwine({ + /** * onafterrefreshalternate is SS-specific jQuery UI hook that is executed * every time the button is rendered (including on initialisation). */ - onbuttonafterrefreshalternate: function() { - if (this.button('option', 'showingAlternate')) { - this.addClass('ss-ui-action-constructive'); - } - else { - this.removeClass('ss-ui-action-constructive'); - } - } - }); - }); - -}(jQuery)); + onbuttonafterrefreshalternate() { + if (this.button('option', 'showingAlternate')) { + this.addClass('ss-ui-action-constructive'); + } else { + this.removeClass('ss-ui-action-constructive'); + } + } + }); +}); ``` ## Summary diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Formfield_Help_Text.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Formfield_Help_Text.md index 12e03d7b8..89318bf3c 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Formfield_Help_Text.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Formfield_Help_Text.md @@ -3,7 +3,7 @@ title: CMS form field help text summary: Add help text to the form fields in the CMS icon: question --- -# How to Show Help Text on CMS Form Fields +# How to show help text on CMS form fields Sometimes you need to express more context for a form field than is suitable for its `