From 43111977130017b257ebeae1a24ef2fc3396c1a9 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Fri, 3 Nov 2023 11:59:12 +1300 Subject: [PATCH] MNT Fix linting issues --- .editorconfig | 5 +- .eslintrc.js | 12 + .github/workflows/ci.yml | 4 +- .markdownlint.yml | 71 +- composer.json | 10 +- .../00_Server_Requirements.md | 88 +- en/00_Getting_Started/02_Composer.md | 76 +- .../03_Environment_Management.md | 10 +- .../04_Directory_Structure.md | 23 +- en/00_Getting_Started/05_Recipes.md | 9 +- en/00_Getting_Started/index.md | 15 +- en/01_Lessons/index.md | 52 +- .../00_Model/01_Data_Model_and_ORM.md | 211 +-- .../00_Model/02_Relations.md | 354 ++-- en/02_Developer_Guides/00_Model/03_Lists.md | 29 +- .../00_Model/04_Data_Types_and_Casting.md | 174 +- .../00_Model/05_Extending_DataObjects.md | 62 +- .../00_Model/06_SearchFilters.md | 60 +- .../00_Model/07_Permissions.md | 32 +- .../00_Model/08_SQL_Select.md | 131 +- .../00_Model/09_Validation.md | 27 +- .../00_Model/10_Versioning.md | 528 +++--- .../00_Model/11_Scaffolding.md | 255 ++- en/02_Developer_Guides/00_Model/12_Indexes.md | 72 +- .../00_Model/13_Managing_Records.md | 16 +- .../How_Tos/Dynamic_Default_Fields.md | 77 +- .../How_Tos/Grouping_DataObject_Sets.md | 79 +- .../00_Model/How_Tos/index.md | 4 +- en/02_Developer_Guides/00_Model/index.md | 4 +- .../01_Templates/01_Syntax.md | 171 +- .../01_Templates/02_Common_Variables.md | 111 +- .../01_Templates/03_Requirements.md | 196 ++- .../01_Templates/04_Rendering_Templates.md | 121 +- .../01_Templates/05_Template_Inheritance.md | 62 +- .../01_Templates/06_Themes.md | 68 +- .../01_Templates/07_Caching.md | 26 +- .../01_Templates/08_Translations.md | 14 +- .../01_Templates/09_Casting.md | 109 +- .../01_Templates/10_Unique_Keys.md | 16 +- .../11_Partial_Template_Caching.md | 116 +- .../How_Tos/01_Navigation_Menu.md | 9 +- .../01_Templates/How_Tos/02_Pagination.md | 66 +- .../How_Tos/03_Disable_Anchor_Links.md | 15 +- .../01_Templates/How_Tos/index.md | 4 +- en/02_Developer_Guides/01_Templates/index.md | 13 +- .../02_Controllers/01_Introduction.md | 221 +-- .../02_Controllers/02_Routing.md | 123 +- .../02_Controllers/03_Access_Control.md | 158 +- .../02_Controllers/04_Redirection.md | 59 +- .../02_Controllers/05_Middlewares.md | 81 +- .../02_Controllers/06_Builtin_Middlewares.md | 2 +- .../02_Controllers/index.md | 22 +- .../03_Forms/00_Introduction.md | 318 ++-- .../03_Forms/01_Validation.md | 240 ++- .../03_Forms/03_Form_Templates.md | 48 +- .../03_Forms/04_Form_Security.md | 55 +- .../03_Forms/05_Form_Transformations.md | 25 +- .../03_Forms/06_Tabbed_Forms.md | 23 +- .../Field_types/01_Common_Subclasses.md | 77 +- .../03_Forms/Field_types/02_DateField.md | 52 +- .../Field_types/03_HTMLEditorField.md | 248 +-- .../03_Forms/Field_types/04_GridField.md | 360 ++-- .../03_Forms/Field_types/index.md | 4 +- .../03_Forms/How_Tos/01_Encapsulate_Forms.md | 136 +- .../03_Forms/How_Tos/02_Lightweight_Form.md | 55 +- .../How_Tos/03_Create_a_GridFieldComponent.md | 41 +- .../04_Create_a_GridField_ActionProvider.md | 115 +- .../How_Tos/05_Simple_Contact_Form.md | 155 +- .../03_Forms/How_Tos/06_Handle_Nested_data.md | 69 +- .../03_Forms/How_Tos/index.md | 4 +- en/02_Developer_Guides/03_Forms/index.md | 14 +- .../04_Configuration/00_Configuration.md | 266 +-- .../04_Configuration/01_SiteConfig.md | 46 +- .../03_Environment_Variables.md | 15 +- .../04_Configuration/index.md | 2 + .../05_Extending/00_Modules.md | 144 +- .../05_Extending/01_Extensions.md | 292 ++-- .../05_Extending/04_Shortcodes.md | 146 +- .../05_Extending/05_Injector.md | 267 +-- .../05_Extending/06_Aspects.md | 131 +- .../05_Extending/07_Templates.md | 6 +- .../How_Tos/01_Publish_a_Module.md | 51 +- .../02_Create_a_Google_Maps_Shortcode.md | 17 +- .../How_Tos/03_Track_member_logins.md | 29 +- .../05_Extending/How_Tos/index.md | 2 +- en/02_Developer_Guides/05_Extending/index.md | 12 +- .../06_Testing/00_Unit_Testing.md | 137 +- .../06_Testing/01_Functional_Testing.md | 72 +- .../06_Testing/02_Behavior_Testing.md | 4 +- .../06_Testing/04_Fixtures.md | 227 +-- .../06_Testing/05_Testing_Glossary.md | 2 + .../How_Tos/00_Write_a_SapphireTest.md | 46 +- .../How_Tos/01_Write_a_FunctionalTest.md | 37 +- .../06_Testing/How_Tos/02_FixtureFactories.md | 37 +- .../06_Testing/How_Tos/03_Testing_Email.md | 33 +- .../06_Testing/How_Tos/index.md | 4 +- en/02_Developer_Guides/06_Testing/index.md | 53 +- .../07_Debugging/00_Environment_Types.md | 33 +- .../07_Debugging/01_Error_Handling.md | 114 +- .../07_Debugging/02_URL_Variable_Tools.md | 70 +- .../07_Debugging/03_Template_debugging.md | 6 +- en/02_Developer_Guides/07_Debugging/index.md | 10 +- .../08_Performance/00_Partial_Caching.md | 51 +- .../08_Performance/01_Caching.md | 74 +- .../08_Performance/02_HTTP_Cache_Headers.md | 132 +- .../08_Performance/03_Profiling.md | 10 +- .../08_Performance/04_Static_Publishing.md | 10 +- .../08_Performance/05_Resource_Usage.md | 12 +- .../08_Performance/index.md | 12 +- .../09_Security/00_Member.md | 114 +- .../09_Security/01_Access_Control.md | 40 +- .../09_Security/02_Permissions.md | 70 +- .../09_Security/03_Authentication.md | 59 +- .../09_Security/04_Secure_Coding.md | 434 ++--- .../09_Security/05_Rate_Limiting.md | 4 +- .../09_Security/06_Personal_Data.md | 16 +- en/02_Developer_Guides/09_Security/index.md | 4 +- en/02_Developer_Guides/10_Email/index.md | 107 +- .../11_Integration/00_CSV_Import.md | 245 +-- .../11_Integration/02_RSSFeed.md | 120 +- .../Import_CSV_through_a_Controller.md | 64 +- .../How_Tos/custom_csvbulkloader.md | 146 +- .../11_Integration/How_Tos/index.md | 4 +- .../11_Integration/index.md | 4 +- .../12_Search/01_Searchcontext.md | 151 +- .../12_Search/02_FulltextSearch.md | 38 +- en/02_Developer_Guides/12_Search/index.md | 2 + en/02_Developer_Guides/13_i18n/index.md | 189 +-- .../14_Files/01_File_Management.md | 90 +- en/02_Developer_Guides/14_Files/02_Images.md | 110 +- .../14_Files/03_File_Security.md | 168 +- .../14_Files/04_File_Storage.md | 56 +- .../14_Files/05_File_Migration.md | 87 +- .../14_Files/06_Allowed_file_types.md | 14 +- .../14_Files/07_File_Usage.md | 11 +- en/02_Developer_Guides/14_Files/index.md | 26 +- .../01_ModelAdmin.md | 310 ++-- .../02_CMS_Architecture.md | 283 ++-- .../03_CMS_Layout.md | 47 +- .../04_Preview.md | 427 +++-- .../05_Typography.md | 39 +- .../06_Javascript_Development.md | 316 ++-- .../07_ReactJS_Redux_and_GraphQL.md | 610 +++---- .../How_Tos/CMS_Alternating_Button.md | 135 +- .../How_Tos/CMS_Formfield_Help_Text.md | 4 +- .../How_Tos/Customise_CMS_Menu.md | 36 +- .../How_Tos/Customise_CMS_Pages_List.md | 49 +- .../How_Tos/Customise_CMS_Tree.md | 59 +- .../How_Tos/Customise_React_Components.md | 118 +- .../How_Tos/Customise_React_Forms.md | 52 +- .../How_Tos/Customise_Site_Reports.md | 57 +- .../How_Tos/Extend_CMS_Interface.md | 79 +- .../Extending_An_Existing_ModelAdmin.md | 16 +- .../How_Tos/index.md | 4 +- .../index.md | 2 + .../16_Execution_Pipeline/01_Flushable.md | 27 +- .../16_Execution_Pipeline/02_Manifests.md | 24 +- .../03_App_Object_and_Kernel.md | 55 +- .../16_Execution_Pipeline/index.md | 52 +- en/02_Developer_Guides/17_CLI/index.md | 37 +- .../18_Cookies_And_Sessions/01_Cookies.md | 54 +- .../18_Cookies_And_Sessions/02_Sessions.md | 43 +- .../18_Cookies_And_Sessions/index.md | 8 +- .../01_activating_the_server.md | 8 +- .../02_configuring_your_schema.md | 109 +- .../03_building_the_schema.md | 22 +- .../04_using_procedural_code.md | 37 +- .../05_deploying_the_schema.md | 2 +- .../19_GraphQL/01_getting_started/index.md | 2 +- .../01_adding_dataobjects_to_the_schema.md | 206 +-- .../02_query_plugins.md | 189 ++- .../03_permissions.md | 24 +- .../04_inheritance.md | 92 +- .../05_versioning.md | 78 +- .../06_property_mapping.md | 28 +- .../07_nested_definitions.md | 18 +- .../02_working_with_dataobjects/index.md | 8 +- .../01_creating_a_generic_type.md | 10 +- .../02_building_a_custom_query.md | 60 +- .../03_resolver_discovery.md | 122 +- .../04_adding_arguments.md | 33 +- .../05_adding_pagination.md | 75 +- .../06_adding_descriptions.md | 12 +- .../07_enums_unions_and_interfaces.md | 49 +- .../03_working_with_generic_types/index.md | 4 +- .../01_authentication.md | 14 +- .../04_security_and_best_practices/02_cors.md | 42 +- .../03_csrf_protection.md | 4 +- .../04_http_method_checking.md | 6 +- .../05_recursive_or_complex_queries.md | 12 +- .../04_security_and_best_practices/index.md | 2 +- .../19_GraphQL/05_plugins/01_overview.md | 95 +- .../05_plugins/02_writing_a_simple_plugin.md | 61 +- .../05_plugins/03_writing_a_complex_plugin.md | 171 +- .../19_GraphQL/05_plugins/index.md | 2 +- .../06_extending/adding_a_custom_model.md | 29 +- .../06_extending/adding_a_custom_operation.md | 70 +- .../06_extending/adding_middleware.md | 30 +- .../19_GraphQL/06_extending/global_schema.md | 10 +- .../19_GraphQL/06_extending/index.md | 4 +- .../19_GraphQL/07_tips_and_tricks.md | 31 +- .../19_GraphQL/08_architecture_diagrams.md | 38 +- en/02_Developer_Guides/19_GraphQL/index.md | 2 +- en/02_Developer_Guides/index.md | 4 +- .../01_Keeping_projects_up_to_date.md | 2 +- en/03_Upgrading/02_Upgrading_module.md | 66 +- en/03_Upgrading/03_Migrating_off_CWP.md | 28 +- en/03_Upgrading/04_Upgrading_project.md | 427 +++-- en/03_Upgrading/05_PHPUnit9.md | 6 +- en/03_Upgrading/06_Upgrading_to_GraphQL_4.md | 140 +- en/03_Upgrading/07_Deprecations.md | 21 +- en/03_Upgrading/07_Upgrading_Fluent.md | 28 +- en/03_Upgrading/index.md | 6 +- en/04_Changelogs/4.0.0.md | 1500 +++++++++-------- en/04_Changelogs/4.0.1.md | 1 + en/04_Changelogs/4.0.2.md | 1 + en/04_Changelogs/4.0.3.md | 1 + en/04_Changelogs/4.0.4.md | 3 +- en/04_Changelogs/4.0.5.md | 7 +- en/04_Changelogs/4.0.6.md | 1 + en/04_Changelogs/4.0.7.md | 1 + en/04_Changelogs/4.1.0.md | 78 +- en/04_Changelogs/4.1.1.md | 3 +- en/04_Changelogs/4.1.2.md | 1 + en/04_Changelogs/4.1.3.md | 7 +- en/04_Changelogs/4.1.4.md | 1 + en/04_Changelogs/4.1.5.md | 1 + en/04_Changelogs/4.10.0.md | 32 +- en/04_Changelogs/4.11.0.md | 46 +- en/04_Changelogs/4.11.1.md | 98 +- en/04_Changelogs/4.12.0.md | 81 +- en/04_Changelogs/4.13.0.md | 26 +- en/04_Changelogs/4.2.0.md | 139 +- en/04_Changelogs/4.2.1.md | 7 +- en/04_Changelogs/4.2.2.md | 1 + en/04_Changelogs/4.2.3.md | 1 + en/04_Changelogs/4.2.4.md | 1 + en/04_Changelogs/4.2.5.md | 1 + en/04_Changelogs/4.3.0.md | 76 +- en/04_Changelogs/4.3.1.md | 1 + en/04_Changelogs/4.3.2.md | 1 + en/04_Changelogs/4.3.3.md | 1 + en/04_Changelogs/4.3.4.md | 1 + en/04_Changelogs/4.3.6.md | 84 +- en/04_Changelogs/4.4.0.md | 638 +++---- en/04_Changelogs/4.4.1.md | 1 + en/04_Changelogs/4.4.2.md | 1 + en/04_Changelogs/4.4.3.md | 41 +- en/04_Changelogs/4.4.4.md | 3 +- en/04_Changelogs/4.4.5.md | 1 + en/04_Changelogs/4.4.6.md | 107 +- en/04_Changelogs/4.4.7.md | 48 +- en/04_Changelogs/4.5.0.md | 326 ++-- en/04_Changelogs/4.5.1.md | 1 + en/04_Changelogs/4.5.2.md | 106 +- en/04_Changelogs/4.5.3.md | 52 +- en/04_Changelogs/4.6.0.md | 87 +- en/04_Changelogs/4.6.1.md | 1 + en/04_Changelogs/4.7.0.md | 14 +- en/04_Changelogs/4.7.1.md | 2 +- en/04_Changelogs/4.7.2.md | 1 + en/04_Changelogs/4.7.3.md | 1 + en/04_Changelogs/4.8.0.md | 18 +- en/04_Changelogs/4.9.0.md | 86 +- en/04_Changelogs/alpha/4.0.0-alpha1.md | 928 +++++----- en/04_Changelogs/alpha/4.0.0-alpha3.md | 1 + en/04_Changelogs/alpha/4.0.0-alpha4.md | 3 +- en/04_Changelogs/alpha/4.0.0-alpha5.md | 3 +- en/04_Changelogs/alpha/4.0.0-alpha6.md | 3 +- en/04_Changelogs/alpha/4.0.0-alpha7.md | 3 +- en/04_Changelogs/alpha/4.5.0-alpha1.md | 1 + en/04_Changelogs/alpha/4.9.0-alpha1.md | 2 +- en/04_Changelogs/beta/4.0.0-beta1.md | 1 + en/04_Changelogs/beta/4.0.0-beta2.md | 1 + en/04_Changelogs/beta/4.0.0-beta3.md | 1 + en/04_Changelogs/beta/4.0.0-beta4.md | 1 + en/04_Changelogs/beta/4.10.0-beta1.md | 26 +- en/04_Changelogs/beta/4.11.0-beta1.md | 34 +- en/04_Changelogs/beta/4.12.0-beta1.md | 71 +- en/04_Changelogs/beta/4.13.0-beta1.md | 19 +- en/04_Changelogs/beta/4.2.0-beta1.md | 1 + en/04_Changelogs/beta/4.6.0-beta1.md | 23 +- en/04_Changelogs/beta/4.7.0-beta1.md | 409 ++--- en/04_Changelogs/beta/4.8.0-beta1.md | 565 +++---- en/04_Changelogs/beta/4.9.0-beta1.md | 55 +- en/04_Changelogs/beta/index.md | 2 +- en/04_Changelogs/index.md | 8 +- en/04_Changelogs/rc/4.0.0-rc1.md | 1 + en/04_Changelogs/rc/4.0.0-rc2.md | 1 + en/04_Changelogs/rc/4.0.0-rc3.md | 1 + en/04_Changelogs/rc/4.0.1-rc1.md | 1 + en/04_Changelogs/rc/4.1.0-rc1.md | 1 + en/04_Changelogs/rc/4.1.0-rc2.md | 1 + en/04_Changelogs/rc/4.10.0-rc1.md | 22 +- en/04_Changelogs/rc/4.11.0-rc1.md | 37 +- en/04_Changelogs/rc/4.12.0-rc1.md | 75 +- en/04_Changelogs/rc/4.13.0-rc1.md | 25 +- en/04_Changelogs/rc/4.3.0-rc1.md | 3 +- en/04_Changelogs/rc/4.4.0-rc1.md | 148 +- en/04_Changelogs/rc/4.5.0-rc1.md | 1 + en/04_Changelogs/rc/4.5.0-rc2.md | 3 +- en/04_Changelogs/rc/4.6.0-rc1.md | 26 +- en/04_Changelogs/rc/4.7.0-rc1.md | 11 +- en/04_Changelogs/rc/4.8.0-rc1.md | 26 +- en/04_Changelogs/rc/4.9.0-rc1.md | 55 +- en/04_Changelogs/rc/index.md | 4 +- en/05_Contributing/00_Issues_and_Bugs.md | 50 +- en/05_Contributing/01_Code.md | 30 +- en/05_Contributing/02_Documentation.md | 82 +- en/05_Contributing/03_Translations.md | 16 +- .../01_PHP_Coding_Conventions.md | 88 +- .../02_JavaScript_Coding_Conventions.md | 18 +- .../03_CSS_Coding_Conventions.md | 34 +- .../05_Coding_Conventions/index.md | 2 +- en/05_Contributing/06_Build_Tooling.md | 12 +- en/05_Contributing/08_Triage_Resources.md | 48 +- en/05_Contributing/09_Release_Process.md | 241 +-- .../10_Managing_Security_Issues.md | 14 +- en/05_Contributing/index.md | 10 +- .../01_Core_committers.md | 46 +- .../02_Code_of_conduct.md | 80 +- .../03_Maintainer_Guidelines.md | 32 +- .../04_Request_for_comment.md | 46 +- .../05_Major_release_policy.md | 20 +- .../06_Minor_release_policy.md | 9 +- .../07_Supported_Modules.md | 6 +- en/06_Project_Governance/08_Public_API.md | 32 +- en/06_Project_Governance/index.md | 2 + en/index.md | 20 +- package.json | 10 +- phpcs.xml | 98 ++ 331 files changed, 13103 insertions(+), 11063 deletions(-) create mode 100644 phpcs.xml diff --git a/.editorconfig b/.editorconfig index 93aa316aa..c0eabf575 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,5 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[*.md] -trim_trailing_whitespace = false - [composer.json] -indent_size = 4 \ No newline at end of file +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js index e69ff0703..3c6095d2a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,12 +13,24 @@ rules.overrides = [ 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', } } ]; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef4f23c70..d74b9db89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: with: node-version: ${{ steps.read-nvm.outputs.version }} - - name: Install yarn and dependencies + - name: Install yarn dependencies run: | npm install --global yarn yarn install @@ -44,7 +44,7 @@ jobs: - name: Install composer dependencies if: ${{ matrix.script == 'lint-php' }} - run: composer install --prefer-dist --no-progress --ansi --no-interaction --no-scripts --no-plugins --optimize-autoloader + run: composer install --prefer-dist --no-progress --ansi --no-interaction --optimize-autoloader - name: Run lint run: yarn ${{ matrix.script }} diff --git a/.markdownlint.yml b/.markdownlint.yml index 711add204..4fcc52be9 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,22 +1,34 @@ # Enable all rules with default settings as a baseline default: true -customRules: - - 'markdownlint-rule-title-case-style' - # markdownlint-rule-title-case-style: Use sentence-style headings title-case-style: case: 'sentence' ignore: + - 'Apache' + - 'APIs' + - 'Composer' - 'GitHub' - 'GraphQL' + - 'Huntr' - 'JavaScript' - 'I' - 'InnoDB' + - 'Git' + - 'jQuery' + - 'jQuery,' + - 'Lighttpd' - 'MyISAM' - 'MySQL' + - 'Nginx' + - 'Nginx,' + - 'PHPUnit' - 'RFCs' - 'Silverstripe' + - 'TinyMCE' + - 'Transifex' + - 'URLs' + - 'WebP' # MD041: Ignore the frontmatter (metadata) title when checking for H1s first-line-h1: @@ -78,27 +90,80 @@ fenced-code-language: allowed_languages: - 'bash' # use this instead of shell or env - 'css' + - 'diff' - 'graphql' + - 'html' - 'js' + - 'json' - 'php' - 'scss' - 'ss' + - 'sql' - 'text' + - 'xml' - 'yml' # MD044: Enforces capitalisation for specific names proper-names: code_blocks: false names: + - 'API' + - '-api' # e.g. "#breaking-api" in anchor links + - '#api' # anchor links + - 'type/api-break' # the GitHub label + - 'CI' + - '-ci' # e.g. "#using-ci" in anchor links - 'CMS' + - '/cms' # e.g. "silverstripe/cms" + - '-cms' # e.g. "silverstripe/recipe-cms" + - '#cms' # anchor links - 'CSS' + - 'GitHub' + - 'GraphQL' + - '#graphql' # anchor links + - '-graphql' # e.g. "#define-graphql" in anchor links + - '/graphql' # e.g. "silverstripe/graphql" + - 'HTTP' - 'JavaScript' - 'JS' + - '#js' # anchor links + - '-js' # e.g. "#history-viewer-js" in anchor links + - '.js' # e.g. "Node.js" + - 'jQuery' + - '#jquery' # anchor links + - 'ORM' + - '#orm' # anchor links + - '-orm' # e.g. "#overview-orm" in anchor links + - 'PHP' + - '-php' # e.g. "#config-php" in anchor links + - 'php-' # e.g. "php-intl extension" + - '#php' # anchor links - 'SCSS' - 'Silverstripe' + - '#silverstripe/' # anchor links + - 'silverstripe/' # e.g. "silverstripe/framework" + - 'silverstripe-' # e.g. "silverstripe-vendormodule" + - 'SilverStripe\' # e.g. api links + - 'silverstripe.com' + - 'silverstripe.org' + - 'TinyMCE' + - '#tinymce' # anchor links + - 'UI' + - '-ui' # e.g. "#installer-ui" in anchor links + - 'URL' + - '-url' # e.g. "#admin-ui" in anchor links + - 'YAML' + - '-yaml' # e.g. "#split-yaml" in anchor links # 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/composer.json b/composer.json index f49d5c396..778dcfe94 100644 --- a/composer.json +++ b/composer.json @@ -21,12 +21,18 @@ } ], "require-dev": { - "silverstripe/markdown-php-codesniffer": "dev-pulls/1/new-repo" + "silverstripe/markdown-php-codesniffer": "dev-pulls/1/new-repo", + "slevomat/coding-standard": "^8.14" }, "repositories": [ { "url": "https://github.com/creative-commoners/markdown-php-codesniffer", "type": "vcs" } - ] + ], + "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 5332a03b6..6c080e3f2 100644 --- a/en/00_Getting_Started/00_Server_Requirements.md +++ b/en/00_Getting_Started/00_Server_Requirements.md @@ -12,26 +12,26 @@ the server to update templates, website logic, and perform upgrades or maintenan ## PHP -* PHP >=7.4, <=8.1 -* 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. `pdo` or `mysqli`) +- PHP >=7.4, <=8.1 +- 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. `pdo` or `mysqli`) Use [phpinfo()](http://php.net/manual/en/function.phpinfo.php) to inspect your configuration. -Silverstripe CMS tracks the official [PHP release support timeline](https://www.php.net/supported-versions.php). When a PHP version reaches end-of-life, Silverstripe CMS drops support for it in the next minor release. +Silverstripe CMS tracks the official [PHP release support timeline](https://www.php.net/supported-versions.php). When a PHP version reaches end-of-life, Silverstripe CMS drops support for it in the next minor release. ## Database -* MySQL >=5.6 ( +- MySQL >=5.6 ( built-in, [commercially supported](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)) -* PostgreSQL ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/postgresql), community +- PostgreSQL ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/postgresql), community supported) -* SQL Server ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/mssql), community supported) -* SQLite ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/sqlite3), community supported) +- SQL Server ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/mssql), community supported) +- SQLite ([third party module](https://addons.silverstripe.org/add-ons/silverstripe/sqlite3), community supported) -### Default MySQL Collation +### Default MySQL collation In Silverstripe CMS Recipe 4.7 and later, 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 @@ -56,18 +56,19 @@ 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.) This setting is only available in Silverstripe CMS 4.7 and later. - ### 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. `INT(8)` and MySQL server reports it simply as `INT`. MySQLSchemaManager has been updated to detect the MySQL server implementation and act accordingly. In cases when auto-detection fails, you can force the desired behaviour like this: + ```yml SilverStripe\ORM\Connect\MySQLSchemaManager: schema_use_int_width: true # or false when INT widths should be ignored ``` -## Webserver Configuration +## Webserver configuration ### Overview @@ -87,16 +88,16 @@ secure hosting via the `public/`. See [4.1.0 upgrading guide](/changelogs/4.1.0) During runtime, Silverstripe CMS needs read access for the webserver user to your base path (including 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 version 4 introduces this directory. This is where your schema is +- `.graphql-generated`: silverstripe/graphql version 4 introduces 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 version 4 introduces this directory. It's used for +- `public/_graphql`: silverstripe/graphql version 4 introduces 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. @@ -124,7 +125,7 @@ the `File.allowed_extensions` setting (see [File Security](/developer_guides/files/file_security#file-types)). This whitelist uses the same defaults configured through file upload through Silverstripe CMS, so is considered a second line of defence. -### 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/`). @@ -141,13 +142,13 @@ PHP. This can be configured via [.env](/getting_started/environment_management) variable, relative to the `index.php` location. -``` +```bash SS_PROTECTED_ASSETS_PATH="../.protected/" ``` The resulting folder structure will look as follows: -``` +```text .protected/ /my-protected-file.txt public/ @@ -160,7 +161,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 @@ -169,19 +170,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 assets copied from the (inaccessible) `vendor/` folder +- `public/_resources/`: Frontend assets copied from the (inaccessible) `vendor/` folder via [silverstripe/vendor-plugin](https://github.com/silverstripe/vendor-plugin). See [Templates: Requirements](/developer_guides/templates/requirements#exposing-assets-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 v4](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 @@ -196,13 +197,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 expects using the `mod_rewrite` module. By default, these 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 @@ -216,7 +217,7 @@ 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. @@ -268,23 +269,25 @@ see [Microsoft IIS and SQL Server configuration](https://forum.silverstripe.org/ 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 SwiftMailer to send email messages. New installations setup with silverstripe/installer are configured to use a `sendmail` found in `/usr/sbin/sendmail` or another location specified via configuration. Alternatively email can be configured 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 = new SilverStripe\Control\Email\Email('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(); ``` @@ -292,8 +295,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: @@ -311,9 +313,9 @@ From Silverstripe CMS 5 onwards, the [Silverstripe CMS major release policy](/pr Silverstripe CMS supports the following web browsers: -* Google Chrome -* Microsoft Edge -* Mozilla Firefox +- Google Chrome +- Microsoft Edge +- Mozilla Firefox We aim to provide satisfactory experiences in Apple Safari. Silverstripe CMS works well across Windows, Linux, and Mac operating systems. diff --git a/en/00_Getting_Started/02_Composer.md b/en/00_Getting_Started/02_Composer.md index a1d0c9c10..6ec2bd195 100644 --- a/en/00_Getting_Started/02_Composer.md +++ b/en/00_Getting_Started/02_Composer.md @@ -18,7 +18,7 @@ the code dependencies you need to run your Silverstripe CMS website from the cod Next, [install composer](https://getcomposer.org/download/). For our documentation we assume the `composer` command is installed globally. You should now be able to run the command: -``` +```bash composer help ``` @@ -27,7 +27,7 @@ composer help Composer can create a new site for you, using the installer as a template. By default it will download the latest stable version: -``` +```bash composer create-project silverstripe/installer my-project ``` @@ -44,7 +44,7 @@ visit the site in your web browser, and the installation process will be complet You can also specify a version to download that version explicitly, i.e. this will download the older `4.3.3` release: -``` +```bash composer create-project silverstripe/installer ./my-project 4.3.3 ``` @@ -54,11 +54,11 @@ see [Using development versions](#using-development-versions). ## Adding modules to your project -Composer isn't only used to download Silverstripe CMS, it is also used to manage all Silverstripe CMS modules. +Composer isn't only used to download Silverstripe CMS, it is also used to manage all Silverstripe CMS modules. You can find thousands of modules on [https://addons.silverstripe.org](https://addons.silverstripe.org). Installing a module can be done with the following command: -``` +```bash composer require silverstripe/blog ``` @@ -66,7 +66,7 @@ This will install the `silverstripe/blog` module in the latest compatible versio want to install already (such as `^2`), you can add it after the package name as a [version constraint](http://getcomposer.org/doc/01-basic-usage.md#the-require-key): -``` +```bash composer require silverstripe/blog ^2 ``` @@ -83,7 +83,7 @@ time. Silverstripe CMS modules are no exception. To get the latest updates of the modules in your project, run this command: -``` +```bash composer update ``` @@ -101,22 +101,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 projects source code. Silverstripe CMS ships with -a [.gitignore](http://git-scm.com/docs/gitignore) file by default which prevents this. For more details +a [`.gitignore`](http://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: @@ -127,7 +127,7 @@ things in order to be able to do this: The first two steps are done as part of the initial create project using additional arguments. -``` +```bash composer create-project --keep-vcs --dev silverstripe/installer ./my-project 4.x-dev --prefer-source ``` @@ -140,9 +140,9 @@ The `--keep-vcs` flag will make sure you have access to the git history of the i The `--dev` flag is optional, and can be used to add a couple modules which are useful for Silverstripe CMS development: -* The `behat-extension` module allows running [Behat](http://behat.org) integration tests -* The `docsviewer` module will let you preview changes to the project documentation -* The `buildtools` module which adds [phing](http://phing.info) tasks for creating Silverstripe CMS releases +- The `behat-extension` module allows running [Behat](http://behat.org) integration tests +- The `docsviewer` module will let you preview changes to the project documentation +- The `buildtools` module which adds [phing](http://phing.info) tasks for creating Silverstripe CMS releases Once the `create-project` command completes, you need to edit the `composer.json` in the project root and remove the `@stable` markers from the `silverstripe/cms` and `silverstripe/framework` version entries. @@ -152,9 +152,9 @@ an existing composer project with these steps. Please read the ["Contributing Code"](/contributing/code) documentation to find out how to create forks and send pull requests. -# Advanced usage +## Advanced usage -## Manually editing composer.json +### Manually editing `composer.json` To remove dependencies, or if you prefer seeing all 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: @@ -182,14 +182,14 @@ modules. Be careful with the commas at the end of the lines! Save your file, and then run the following command to refresh the installed packages: -``` +```bash composer update ``` -## Using development versions +### 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 @@ -199,34 +199,34 @@ contribute back to the Silverstripe CMS project, it also allows you to get fixes This is a two step process. First you get composer to start a project based on the latest unstable silverstripe/installer -``` +```bash composer create-project silverstripe/installer ./my-project dev-master ``` Or for the latest development version in the 4.0.x series -``` +```bash composer create-project silverstripe/installer ./my-project 4.0.x-dev ``` -## Working with project forks and unreleased modules +### Working with project forks and unreleased modules 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 @@ -242,10 +242,10 @@ This is how you do it: } ``` -* **Install the module as you would normally.** Use the regular composer function - there are no special flags to use a +- **Install the module as you would normally.** Use the regular composer function - there are no special flags to use a fork. Your fork will be used in place of the package version. -``` +```bash composer require silverstripe/cms ``` @@ -255,7 +255,7 @@ the `repositories` section from `composer.json` and run `composer update`. Now add an "upstream" remote to the original repository location so you can rebase or merge your fork as required. -``` +```bash cd cms git remote add -f upstream git://github.com/silverstripe/silverstripe-cms.git ``` @@ -263,7 +263,7 @@ git remote add -f upstream git://github.com/silverstripe/silverstripe-cms.git For more information, read the ["Repositories" chapter of the Composer documentation](http://getcomposer.org/doc/05-repositories.md). -### Forks and branch names +#### Forks and branch names Generally, you should keep using the same pattern of branch names as the main repositories does. If your version is a fork of 4.0, then call the branch `4.0`, not `4.0-myproj` or `myproj`. Otherwise, the dependency resolution gets @@ -324,7 +324,7 @@ on [packagist.org](http://packagist.org). Follow the packagist.org advice on choosing a [unique name and vendor prefix](https://packagist.org/about). 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-module". ### What about themes? @@ -345,14 +345,14 @@ the live server hosts a git repository checkout, which is updated to push a newe run `composer install` afterwards. We recommend looking into [Composer "lock" files](http://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](http://getcomposer.org/doc/01-basic-usage.md#autoloading) or [scripts](http://getcomposer.org/doc/articles/scripts.md) which some modules will start relying on. Please check the module README for specific installation instructions. -### 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"](http://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 c972e4580..e9eaa7329 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 @@ -65,7 +65,7 @@ Environment::setEnv('API_KEY', 'AABBCCDDEEFF012345'); To use environment variables in `.yaml` configs you can reference them using backticks. You can have multiple environment variables within a single value, though the overall value must start and end with backticks. -```yaml +```yml SilverStripe\Core\Injector\Injector: MyServiceClass: properties: @@ -78,7 +78,7 @@ SilverStripe\Core\Injector\Injector: Environment variables cannot be used outside of Injector config as of version 4.2. [/info] -## 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: @@ -127,7 +127,7 @@ 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 | | `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name | | `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) | +| `SS_BASE_URL` | The URL to use when it isn't determinable by other means (eg: for CLI commands) | | `SS_DATABASE_SSL_KEY` | Absolute path to SSL key file | | `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file | | `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file | diff --git a/en/00_Getting_Started/04_Directory_Structure.md b/en/00_Getting_Started/04_Directory_Structure.md index 748a56bf4..236c2eda4 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,30 +22,29 @@ Directory | Description `vendor/` | Silverstripe modules and other supporting libraries (the framework is in `vendor/silverstripe/framework`) `themes/` | Standard theme installation location -## Custom Code Structure +## Custom code structure We're using `app/` as the default folder. Note that until Silverstripe 4.2, this directory was named `mysite/`, and PHP code was stored in a `code/` rather than `src/` folder. | Directory | Description | - | --------- | ----------- | +| --------- | ----------- | | `app/` | This directory contains all of your code that defines your website. | | `app/_config` | YAML configuration specific to your application | | `app/src` | PHP code for model and controller (subdirectories are optional) | | `app/tests` | PHP Unit tests | | `app/templates` | HTML [templates](/developer_guides/templates) with *.ss-extension for the `$default` theme | -| `app/css ` | CSS files | -| `app/images ` | Images used in the HTML templates | -| `app/javascript` | Javascript and other script files | +| `app/css` | CSS files | +| `app/images` | Images used in the HTML templates | +| `app/javascript` | JavaScript and other script files | | `app/client` | More complex projects can alternatively contain frontend assets in a common `client` folder | | `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 --- @@ -56,7 +55,7 @@ SilverStripe\Core\Manifest\ModuleManifest: Check our [JavaScript Coding Conventions](javascript_coding_conventions) for more details on folder and file naming in Silverstripe core modules. -## Themes Structure +## Themes structure | Directory | Description | | ------------------ | --------------------------- | @@ -67,7 +66,7 @@ Silverstripe core modules. See [themes](/developer_guides/templates/themes). -## Module Structure {#module_structure} +## 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". @@ -115,7 +114,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 8538a1a7b..a03f581f4 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` 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` recipe installs `silverstripe/blog` module, but also: + - `silverstripe/widgets` and `silverstripe/content-widget` to display widgets - `silverstripe/comments` and `silverstripe/comment-notifications` to allow the management of comments on blog post - `silverstripe/spamprotection` and `silverstripe/akismet` 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 bdedbd6fe..bff628ea9 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 7.4 or newer. It runs on many webservers and databases, but is most commonly served using Apache and MySQL/MariaDB. @@ -12,14 +14,14 @@ 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 you install and upgrade the framework and other modules. Assuming you've got this tool, run the following command to install Silverstripe: -``` +```bash composer create-project silverstripe/installer my-project ``` @@ -28,7 +30,7 @@ Within the newly created `my-project` folder, point your webserver at the `publi Now create a `.env` file your project root (not the `public/` folder). It sets up the minimum required [environment variables](environment_management). Replace the placeholders as required: -``` +```bash SS_DATABASE_CLASS="MySQLDatabase" SS_DATABASE_NAME="" SS_DATABASE_SERVER="localhost" @@ -41,7 +43,7 @@ SS_ENVIRONMENT_TYPE="" Now you should be able to build your database by running this command: -``` +```bash vendor/bin/sake dev/build ``` @@ -50,7 +52,7 @@ Your website should be available on your domain now (e.g. `http://localhost`). T 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 @@ -64,4 +66,3 @@ Webserver setup is covered in If you run into trouble, see [the Tips & Tricks forum](https://forum.silverstripe.org/c/tips) or get help on our [Slack channel](https://www.silverstripe.org/community/slack-signup/). - diff --git a/en/01_Lessons/index.md b/en/01_Lessons/index.md index 8d9a36811..b8481db92 100644 --- a/en/01_Lessons/index.md +++ b/en/01_Lessons/index.md @@ -4,31 +4,33 @@ 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](http://www.silverstripe.org/community/): Join our community chat via Slack, or ask a question +## Help: if you get stuck + +- [The Tips & Tricks forum](https://forum.silverstripe.org/c/tips): Review some existing solutions to common problems. +- [Silverstripe CMS Community](http://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 7e1aa27ed..ce9efee4e 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](http://en.wikipedia.org/wiki/Object-relational_mapping) 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"](http://en.wikipedia.org/wiki/Active_record_pattern) @@ -20,18 +20,19 @@ 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 $db = [ 'PlayerNumber' => 'Int', 'FirstName' => 'Varchar(255)', 'LastName' => 'Text', - 'Birthday' => 'Date' + 'Birthday' => 'Date', ]; } ``` @@ -39,30 +40,29 @@ class Player extends DataObject This `Player` class definition will create a database table `Player` with columns for `PlayerNumber`, `FirstName` and so on. After writing this class, we need to regenerate the database schema. -## 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 the URL http://www.yoursite.com/dev/build while authenticated as an administrator. +database schema is generated automatically by visiting the URL while authenticated as an administrator. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required. It will perform the following changes: -* Create any missing tables -* Create any missing fields -* Create any missing indexes -* Alter the field type of any existing fields -* Rename any obsolete tables that it previously created to _obsolete_(tablename) +- Create any missing tables +- Create any missing fields +- Create any missing indexes +- Alter the field type of any existing fields +- Rename any obsolete tables that it previously created to `_obsolete_(tablename)` It **won't** do any of the following -* Delete tables -* Delete fields -* Rename any tables that it doesn't recognize. This allows other applications to coexist in the same database, as long as +- Delete tables +- Delete fields +- 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. - [notice] You need to be logged in as an administrator to perform this command, unless your site is in [dev mode](../debugging), or the command is run through [CLI](../cli). @@ -71,23 +71,26 @@ or the command is run through [CLI](../cli). When rebuilding the database schema through the [ClassLoader](api:SilverStripe\Core\Manifest\ClassLoader) the following additional properties 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. -* Created: A date/time field set to the creation date of this record -* LastEdited: A date/time field set to the date this record was last edited through `write()` - -**app/src/Player.php** +- 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. +- Created: A date/time field set to the creation date of this record +- LastEdited: A date/time field set to the date this record was last edited through `write()` ```php +// app/src/Model/Player.php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { + private static string $table_name = 'Player'; + private static $db = [ 'PlayerNumber' => 'Int', 'FirstName' => 'Varchar(255)', 'LastName' => 'Text', - 'Birthday' => 'Date' + 'Birthday' => 'Date', ]; } ``` @@ -110,7 +113,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` syntax. @@ -128,12 +131,11 @@ $player = Player::create(); Using the `create()` method provides chainability, which can add elegance and brevity to your code, e.g. `Player::create()->write()`. More importantly, however, it will look up the class in the [Injector](../extending/injector) so that the class can be overridden by [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection). [/notice] - Database columns and properties 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; ``` @@ -151,7 +153,7 @@ $player = Player::create(); $id = $player->write(); ``` -## Querying Data +## Querying data With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides shortcuts and methods for fetching, sorting and filtering data from our database. @@ -175,11 +177,10 @@ are `filter()` and `sort()`: ```php $members = Player::get()->filter([ - 'FirstName' => 'Sam' + 'FirstName' => 'Sam', ])->sort('Surname'); // returns a `DataList` containing all the `Player` records that have the `FirstName` of 'Sam' - ``` [info] @@ -192,7 +193,7 @@ Provided `filter` values are automatically escaped and do not require any escapi `DataObject::get_by_id()` is a legacy ORM method, and it is recommended that you use `DataObject::get()->byID()` wherever possible [/info] -## Lazy Loading +## Lazy loading The `ORM` doesn't actually execute the [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) until you iterate on the result with a `foreach()` or `<% loop %>`. @@ -201,7 +202,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'); @@ -214,7 +215,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 @@ -229,7 +230,7 @@ echo $players->Count(); ```php $players = Player::get(); -foreach($players as $player) { +foreach ($players as $player) { echo $player->FirstName; } ``` @@ -239,14 +240,14 @@ Notice that we can step into the loop safely without having to check if `$player ```php $players = Player::get(); -if($players->exists()) { +if ($players->exists()) { // do something here } ``` See the [Lists](lists) documentation for more information on dealing with [SS_List](api:SilverStripe\ORM\SS_List) instances. -## Returning a single DataObject +## Returning a single `DataObject` There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you can use `byID($id)`: @@ -291,24 +292,24 @@ However you might have several entries with the same `FirstName` and would like ```php $players = Players::get()->sort([ 'FirstName' => 'ASC', - 'LastName'=>'ASC' + 'LastName' => 'ASC', ]); ``` You can also sort randomly. Using the `DB` class, you can get the random sort method per database type. ```php -$random = DB::get_conn()->random(); +$random = DB::get_conn()->random(); $players = Player::get()->sort($random); ``` -## Filtering Results +## Filtering results The `filter()` method filters the list of objects that gets returned. ```php $players = Player::get()->filter([ - 'FirstName' => 'Sam' + 'FirstName' => 'Sam', ]); ``` @@ -338,9 +339,7 @@ $players = Player::get()->filter('FirstName', 'Sam'); Or if you want to find both Sam and Sig. ```php -$players = Player::get()->filter( - 'FirstName', ['Sam', 'Sig'] -); +$players = Player::get()->filter('FirstName', ['Sam', 'Sig']); // SELECT * FROM Player WHERE FirstName IN ('Sam', 'Sig') ``` @@ -355,7 +354,7 @@ $players = Player::get()->filter([ ]); ``` -### filterAny +### `filterAny` Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive), @@ -400,7 +399,6 @@ checks to ensure that exclusion filters behave predictably. For instance, the below code will select only values that do not match the given value, including nulls. - ```php $players = Player::get()->filter('FirstName:not', 'Sam'); // ... WHERE "FirstName" != 'Sam' OR "FirstName" IS NULL @@ -409,7 +407,6 @@ $players = Player::get()->filter('FirstName:not', 'Sam'); If null values should be excluded, include the null in your check. - ```php $players = Player::get()->filter('FirstName:not', ['Sam', null]); // ... WHERE "FirstName" != 'Sam' AND "FirstName" IS NOT NULL @@ -419,7 +416,6 @@ $players = Player::get()->filter('FirstName:not', ['Sam', null]); It is also often useful to filter by all rows with either empty or null for a given field. - ```php $players = Player::get()->filter('FirstName', [null, '']); // ... WHERE "FirstName" == '' OR "FirstName" IS NULL @@ -444,7 +440,7 @@ $teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15); $teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300); ``` -### filterByCallback +### `filterByCallback` It is also possible to filter by a PHP callback, this will force the data model to fetch all records and loop them in PHP, thus `filter()` or `filterAny()` are to be preferred over `filterByCallback()`. @@ -461,12 +457,12 @@ for each record, if the callback returns true, this record will be added to the The below example will get all `Players` aged over 10. ```php -$players = Player::get()->filterByCallback(function($item, $list) { +$players = Player::get()->filterByCallback(function ($item, $list) { return ($item->Age() > 10); }); ``` -### Exclude +### `exclude` The `exclude()` method is the opposite to the filter in that it removes entries from a list. @@ -480,7 +476,7 @@ Remove both Sam and Sig.. ```php $players = Player::get()->exclude([ - 'FirstName' => ['Sam','Sig'] + 'FirstName' => ['Sam','Sig'], ]); ``` @@ -508,7 +504,7 @@ And removing Sig and Sam with that are either age 17 or 43. ```php $players = Player::get()->exclude([ 'FirstName' => ['Sam', 'Sig'], - 'Age' => [17, 43] + 'Age' => [17, 43], ]); // SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43')); @@ -519,11 +515,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()` @@ -543,7 +539,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. @@ -565,7 +561,7 @@ $members = Member::get()->sort('Surname')->limit(10, 4); Note that the `limit` argument order is different from a MySQL LIMIT clause. [/alert] -### Mapping classes to tables with DataObjectSchema +### Mapping classes to tables with `DataObjectSchema` Note that in most cases, the underlying database table for any DataObject instance will be the same as the class name. However in cases where dealing with namespaced classes, especially when using DB schema which don't support @@ -573,12 +569,12 @@ slashes in table names, it is necessary to provide an alternate mapping. For instance, the below model will be stored in the table name `BannerImage` - ```php namespace SilverStripe\BannerManager; + use SilverStripe\ORM\DataObject; -class BannerImage extends DataObject +class BannerImage extends DataObject { private static $table_name = 'BannerImage'; } @@ -595,30 +591,29 @@ equivalent version. Methods which return class names: -* `tableClass($table)` Finds the class name for a given table. This also handles suffixed tables such as `Table_Live`. -* `baseDataClass($class)` Returns the base data class for the given class. -* `classForField($class, $field)` Finds the specific class that directly holds the given field +- `tableClass($table)` Finds the class name for a given table. This also handles suffixed tables such as `Table_Live`. +- `baseDataClass($class)` Returns the base data class for the given class. +- `classForField($class, $field)` Finds the specific class that directly holds the given field Methods which return table names: -* `tableName($class)` Returns the table name for a given class or object. -* `baseDataTable($class)` Returns the base data class for the given class. -* `tableForField($class, $field)` Finds the specific class that directly holds the given field and returns the table. +- `tableName($class)` Returns the table name for a given class or object. +- `baseDataTable($class)` Returns the base data class for the given class. +- `tableForField($class, $field)` 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. For example, if running a query against a particular model, you will need to ensure you use the correct 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(); @@ -645,21 +640,21 @@ You can specify a WHERE clause fragment (that will be combined with other filter $members = Member::get()->where("\"FirstName\" = 'Sam'"); ``` -#### Joining Tables +#### Joining tables You can specify a join with the `innerJoin` and `leftJoin` methods. Both 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() - ->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); + ->innerJoin('Group_Members', '"Rel"."MemberID" = "Member"."ID"', 'Rel'); ``` [alert] @@ -667,19 +662,20 @@ Passing a *$join* statement will filter results further by the JOINs performed a **not** return the additionally joined data. [/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 object is created. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { - private static $defaults = [ - "Status" => 'Active', + 'Status' => 'Active', ]; } ``` @@ -698,16 +694,26 @@ time. For example, suppose we have the following set of classes: ```php -use SilverStripe\CMS\Model\SiteTree; +namespace { -class Page extends SiteTree -{ + use SilverStripe\CMS\Model\SiteTree; + class Page extends SiteTree + { + // ... + } } -class NewsPage extends Page +``` + +```php +namespace App\PageType; + +use Page; + +class NewsPage extends Page { private static $db = [ - 'Summary' => 'Text' + 'Summary' => 'Text', ]; } ``` @@ -722,7 +728,8 @@ SilverStripe\CMS\Model\SiteTree: LastEdited: Datetime Title: Varchar Content: Text -NewsPage: + +App\PageType\NewsPage: ID: Int Summary: Text ``` @@ -732,39 +739,39 @@ Accessing the data is transparent to the developer. ```php $news = NewsPage::get(); -foreach($news as $article) { +foreach ($news as $article) { echo $article->Title; } ``` 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 have +- "Base classes" are direct sub-classes of [DataObject](api:SilverStripe\ORM\DataObject). They are always given a table, whether or not they have special fields. This is called the "base table". In our case, `SiteTree` is the base table. -* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible +- The base table's ClassName field is set to class of the given record. It's an enumeration of all possible sub-classes of the base class (including the base class itself). -* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the +- Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the example above, NewsSection didn't have its own data, 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 Page refers to the same object as record #2 in [SiteTree](api:SilverStripe\CMS\Model\SiteTree). To retrieve a news article, Silverstripe CMS joins the [SiteTree](api:SilverStripe\CMS\Model\SiteTree), [Page](api:SilverStripe\CMS\Model\SiteTree\Page) and NewsPage tables by their ID fields. -## 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 2e541014f..f8ae360ad 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,29 +13,39 @@ 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-1 and 1-to-1 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 { private static $has_one = [ - "Team" => Team::class, + '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, ]; + // ... } ``` @@ -45,16 +55,19 @@ and provides a short syntax for accessing the related object. To create a has_one/has_many relationship to core classes (File, Image, etc), reference the Classname::class, like below. ```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_many = [ 'Teamphoto' => Image::class, - 'Lineup' => File::class - ]; + 'Lineup' => File::class, + ]; + // ... } ``` @@ -80,7 +93,7 @@ The relationship can also be navigated in [templates](../templates). <% end_with %> ``` -## Polymorphic has_one +## Polymorphic `has_one` A has_one 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. @@ -88,33 +101,50 @@ 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. ```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; // Type of object returned by $fan->FanOf() will vary class Fan extends DataObject { - // Generates columns FanOfID and FanOfClass private static $has_one = [ - "FanOf" => DataObject::class, + 'FanOf' => DataObject::class, ]; + // ... } ``` @@ -122,25 +152,29 @@ 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] -## has_many +## `has_many` Defines 1-to-many joins. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`. [alert] Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors available on both ends. To add a $has_one-relationship on core classes, yml config settings can be used: + ```yml SilverStripe\Assets\Image: has_one: - MyDataObject: MyDataObject + App\Model\MyDataObject: MyDataObject ``` + [/alert] ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Team extends DataObject @@ -152,13 +186,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, + 'Team' => Team::class, ]; + // ... } ``` @@ -174,7 +216,7 @@ echo $team->Players(); echo $team->Players()->Count(); // returns '14'; -foreach($team->Players() as $player) { +foreach ($team->Players() as $player) { echo $player->FirstName; } ``` @@ -182,21 +224,32 @@ foreach($team->Players() as $player) { To specify multiple `$has_many` to the same object 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 { private static $has_one = [ - "Manager" => Person::class, - "Cleaner" => Person::class, + 'Manager' => Person::class, + 'Cleaner' => Person::class, ]; + // ... } ``` @@ -208,15 +261,24 @@ You can use `RelationValidationService` for validation of relationships. This to 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 by their IDs: ```php -public function getCMSFields() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class Company extends DataObject { - $fields = parent::getCMSFields(); - $fields->removeByName(['ManagerID', 'CleanerID']); - return $fields; + // ... + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + $fields->removeByName(['ManagerID', 'CleanerID']); + return $fields; + } } ``` -## belongs_to +## `belongs_to` Defines a 1-to-1 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 @@ -229,25 +291,34 @@ This is not mandatory unless the relationship would be otherwise ambiguous. You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. ```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', ]; + // ... } ``` -## many_many +## `many_many` Defines many-to-many joins, which uses a third table created between the two to join pairs. There are two ways in which this can be declared, which are described below, depending on @@ -259,7 +330,7 @@ to have the necessary accessors available on both ends. You can use `RelationVal Example configuration: -```yaml +```yml SilverStripe\Dev\Validation\RelationValidationService: output_enabled: true ``` @@ -299,7 +370,7 @@ $team->Supporters()->add($supporter2); $team->Supporters()->setExtraData($supporter2->ID, ['Ranking' => 2]); ``` -### 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 (this-class)_(relationship-name), will @@ -309,54 +380,65 @@ 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 { private static $many_many = [ - "Supporters" => Supporter::class, + 'Supporters' => Supporter::class, ]; 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, + '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: '"Team_Supporter"."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 relationship joined on a separate `DataObject` 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](/developer_guides/model/versioning). - - Allows support of other extensions on the mapping table (e.g. subsites). - - Extra fields can be managed separately to the joined dataobject, even via a separate +- Allows versioning of the mapping table, including support for the + [ownership API](/developer_guides/model/versioning). +- Allows support of other extensions on the mapping table (e.g. subsites). +- Extra fields can be managed separately to the joined dataobject, even via a separate GridField or form. This is declared via array syntax, with the following keys on the many_many: - - `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. + +- `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. Just like any normal DataObject, you can apply a default sort which will be applied when accessing many many through relations. @@ -367,18 +449,28 @@ or child record. The syntax for `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 { // Prior to 4.2.0, this also needs to include the reverse relation name via dot-notation @@ -386,7 +478,15 @@ class Supporter extends DataObject private static $belongs_many_many = [ 'Supports' => Team::class, ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class TeamSupporter extends DataObject { private static $db = [ @@ -399,6 +499,7 @@ class TeamSupporter extends DataObject ]; private static $default_sort = '"TeamSupporter"."Ranking" ASC'; + // ... } ``` @@ -413,31 +514,42 @@ $supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]); Note: ->filter() currently does not support joined fields natively due to the fact that the query for the join table is isolated from the outer query controlled by DataList. -### Polymorphic many_many (Experimental) +### Polymorphic `many_many` (experimental) Using many_many through, it is possible to support polymorphic relations on the mapping table. Note, that this feature is currently experimental, and has certain limitations: - - This feature only works with many_many through - - This feature will only allow polymorphic many_many, but not belongs_many_many. However, + +- This feature only works with many_many through +- This feature will only allow polymorphic many_many, but not belongs_many_many. However, you can have a has_many relation to the mapping table on this side, and iterate through this to collate parent records. 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 { // This same many_many may also exist on other classes private static $many_many = [ - "Tags" => [ + 'Tags' => [ 'through' => TagMapping::class, 'from' => 'Parent', 'to' => 'Tag', - ] + ], ]; + // ... } +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + class Tag extends DataObject { // has_many works, but belongs_many_many will not @@ -448,26 +560,35 @@ class Tag extends DataObject /** * Example iterator placeholder for belongs_many_many. * This is a list of arbitrary types of objects - * @return Generator|DataObject[] + * @return \Generator|DataObject[] */ - 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 in templates +### Using `many_many` in templates The relationship can also be navigated in [templates](../templates). The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only) @@ -483,7 +604,7 @@ The joined record can be accessed via `Join` or `TeamSupporter` property (many_m You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template is class-agnostic and doesn't know the type of the join table. -## belongs_many_many +## `belongs_many_many` The belongs_many_many represents the other side of the relationship on the target data class. When using either a basic many_many or a many_many through, the syntax for belongs_many_many is the same. @@ -491,25 +612,33 @@ When using either a basic many_many or a many_many through, the syntax for belon To specify multiple $many_manys between the same classes, specify use the dot notation to distinguish them like below: - ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; class Category extends DataObject { - private static $many_many = [ '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', ]; + // ... } ``` @@ -519,24 +648,37 @@ the best way to think about it is that the object where the relationship will be Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa. - ## Cascading deletions Relationships between objects can cause cascading deletions, if necessary, through configuration of the `cascade_deletes` config on the parent class. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class ParentObject extends DataObject { +class ParentObject extends DataObject +{ private static $has_one = [ 'Child' => ChildObject::class, ]; + private static $cascade_deletes = [ 'Child', ]; + // ... } -class ChildObject extends DataObject { +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class ChildObject extends DataObject +{ + // ... } ``` @@ -550,7 +692,7 @@ See the [versioning docs](/developer_guides/model/versioning) for more informati [alert] Declaring cascade_deletes implies delete permissions on the listed objects. -Built-in controllers using delete operations check canDelete() on the owner, but not on the owned object. +Built-in controllers using delete operations check canDelete() on the owner, but not on the owned object. [/alert] ## Cascading duplications @@ -560,32 +702,47 @@ When you invoke `$dataObject->duplicate()`, relation names specified by this con and saved against the new clone object. Note that duplications will act differently depending on the kind of relation: - - Exclusive relationships (e.g. has_many, belongs_to) will be explicitly duplicated. - - Non-exclusive many_many will not be duplicated, but the mapping table values will instead + +- Exclusive relationships (e.g. has_many, belongs_to) will be explicitly duplicated. +- Non-exclusive many_many will not be duplicated, but the mapping table values will instead be copied for this record. - - Non-exclusive has_one relationships are not normally necessary to duplicate, since both parent and clone +- Non-exclusive has_one relationships are not normally necessary to duplicate, since both parent and clone can normally share the same relation ID. However, if this is declared in `cascade_duplicates` any has one will be similarly duplicated as though it were an exclusive relationship. For example: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class ParentObject extends DataObject { +class ParentObject extends DataObject +{ private static $many_many = [ 'RelatedChildren' => ChildObject::class, ]; + private static $cascade_duplicates = [ 'RelatedChildren' ]; + // ... } -class ChildObject extends DataObject { +``` + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class ChildObject extends DataObject +{ + // ... } ``` When duplicating objects you can disable recursive duplication by passing in `false` to the second argument of duplicate(). -E.g. +For example: ```php $parent = ParentObject::get()->first(); @@ -602,15 +759,15 @@ 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. $team->Supporters()->add($supporter); ``` -## 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. @@ -618,20 +775,22 @@ You can use the ORM to get a filtered result list without writing any SQL. For e See [DataObject::$has_many](api:SilverStripe\ORM\DataObject::$has_many) for more info on the described relations. ```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] @@ -639,7 +798,7 @@ Adding new records to a filtered `RelationList` like in the example above doesn' criteria on the added record. [/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` when saving the `DataObject` for the first @@ -651,25 +810,26 @@ As these lists are not backed by the database, most of the filtering methods on this type. As such, an `UnsavedRelationList` should only be used for setting a relation before saving an object, not for displaying the objects contained in the relation. -## 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 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 documentation -* [Introduction to the Data Model and ORM](data_model_and_orm) -* [Lists](lists) +- [Introduction to the Data Model and ORM](data_model_and_orm) +- [Lists](lists) -## API Documentation +## API documentation -* [HasManyList](api:SilverStripe\ORM\HasManyList) -* [ManyManyList](api:SilverStripe\ORM\ManyManyList) -* [DataObject](api:SilverStripe\ORM\DataObject) -* [LinkTracking](api:SilverStripe\CMS\Model\SiteTreeLinkTracking) +- [HasManyList](api:SilverStripe\ORM\HasManyList) +- [ManyManyList](api:SilverStripe\ORM\ManyManyList) +- [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 43ab6e0d7..5f0bb7886 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 @@ -19,7 +19,7 @@ use SilverStripe\Security\Member; $members = Member::get(); -foreach($members as $member) { +foreach ($members as $member) { echo $member->Name; } ``` @@ -37,7 +37,6 @@ Or in the template engine: ```php // $list->find($key, $value); -// $members = Member::get(); echo $members->find('ID', 4)->FirstName; @@ -111,9 +110,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 @@ -131,13 +131,14 @@ echo $list->Count(); // returns '2' ``` -## 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 85e955e53..37079ade1 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,70 +4,73 @@ 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 or simply a method that returns data for the template. +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 or simply a method that returns data for the template. -A Data Type is represented in Silverstripe CMS by a [DBField](api:SilverStripe\ORM\FieldType\DBField) subclass. The class is responsible for telling the ORM +A Data Type is represented in Silverstripe CMS by a [DBField](api:SilverStripe\ORM\FieldType\DBField) subclass. The class is responsible for telling the ORM about how to store its data in the database and how to format the information coming out of the database, i.e. on a template. In the `Player` example, we have four database columns each with a different data type (Int, Varchar). -**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 +## Available types -* `'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 (see: [DBCurrency](api:SilverStripe\ORM\FieldType\DBCurrency)). -* `'Date'`: A date field (see: [DBDate](api:SilverStripe\ORM\FieldType\DBDate)). -* `'Decimal'`: A decimal number (see: [DBDecimal](api:SilverStripe\ORM\FieldType\DBDecimal)). -* `'Enum'`: An enumeration of a set of strings (see: [DBEnum](api:SilverStripe\ORM\FieldType\DBEnum)). -* `'HTMLText'`: A variable-length string of up to 2MB, designed to store HTML (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). -* `'HTMLVarchar'`: A variable-length string of up to 255 characters, designed to store HTML (see: [DBHTMLVarchar](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)). -* `'Int'`: An integer field (see: [DBInt](api:SilverStripe\ORM\FieldType\DBInt)). -* `'Percentage'`: A decimal number between 0 and 1 that represents a percentage (see: [DBPercentage](api:SilverStripe\ORM\FieldType\DBPercentage)). -* `'Datetime'`: A date / time field (see: [DBDatetime](api:SilverStripe\ORM\FieldType\DBDatetime)). -* `'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)). +- `'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 (see: [DBCurrency](api:SilverStripe\ORM\FieldType\DBCurrency)). +- `'Date'`: A date field (see: [DBDate](api:SilverStripe\ORM\FieldType\DBDate)). +- `'Decimal'`: A decimal number (see: [DBDecimal](api:SilverStripe\ORM\FieldType\DBDecimal)). +- `'Enum'`: An enumeration of a set of strings (see: [DBEnum](api:SilverStripe\ORM\FieldType\DBEnum)). +- `'HTMLText'`: A variable-length string of up to 2MB, designed to store HTML (see: [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText)). +- `'HTMLVarchar'`: A variable-length string of up to 255 characters, designed to store HTML (see: [DBHTMLVarchar](api:SilverStripe\ORM\FieldType\DBHTMLVarchar)). +- `'Int'`: An integer field (see: [DBInt](api:SilverStripe\ORM\FieldType\DBInt)). +- `'Percentage'`: A decimal number between 0 and 1 that represents a percentage (see: [DBPercentage](api:SilverStripe\ORM\FieldType\DBPercentage)). +- `'Datetime'`: A date / time field (see: [DBDatetime](api:SilverStripe\ORM\FieldType\DBDatetime)). +- `'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)). -See the [API documentation](api:SilverStripe\ORM\FieldType\DBField) for a full list of available Data Types. You can define your own [DBField](api:SilverStripe\ORM\FieldType\DBField) instances if required as well. +See the [API documentation](api:SilverStripe\ORM\FieldType\DBField) 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 +## Default values ### Default values for new objects -For complex default values for newly instantiated objects see [Dynamic Default Values](how_tos/dynamic_default_fields). +For complex default values for newly instantiated objects see [Dynamic Default Values](how_tos/dynamic_default_fields). For simple values you can make use of the `$defaults` array. For example: ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Car extends DataObject -{ +class Car extends DataObject +{ private static $db = [ 'Wheels' => 'Int', - 'Condition' => 'Enum(["New","Fair","Junk"])' + 'Condition' => 'Enum(["New","Fair","Junk"])', ]; - + private static $defaults = [ 'Wheels' => 4, - 'Condition' => 'New' + 'Condition' => 'New', ]; } ``` @@ -77,8 +80,8 @@ class Car extends DataObject When adding a new `$db` field to a DataObject you can specify a default value to be applied to all existing records when the column is added in the database for the first time. This will also be applied to any newly created objects -going forward. You do this by passing an argument for the default value in your -`$db` items. +going forward. 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. For string values, you will need to declare this default using the options array. @@ -87,37 +90,40 @@ 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"], "New")', 'Make' => 'Varchar(["default" => "Honda"])', - ); + ]; } ``` -## Formatting Output +## Formatting output The Data Type does more than setup 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. +output. You can manually create instances of a Data Type and pass it through to the template. 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); } } ``` @@ -125,6 +131,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); echo $player->Name; @@ -142,88 +150,103 @@ echo $player->getName()->LimitCharacters(2); Rather than manually returning objects from your custom functions. You can use the `$casting` property. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { private static $casting = [ - "Name" => 'Varchar', + 'Name' => 'Varchar', ]; - - public function getName() + + public function getName() { - return $this->FirstName . ' '. $this->LastName; + return $this->FirstName . ' ' . $this->LastName; } } ``` -The properties on any Silverstripe CMS object can be type casted automatically, 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) +The properties on any Silverstripe CMS object can be type casted automatically, 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. -On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a +On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a number. ```php use SilverStripe\ORM\FieldType\DBField; -DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23 +// results in 1.23 +DBField::create_field('Double', 1.23456)->Round(2); ``` -Of course that's much more verbose than the equivalent PHP call. The power of [DBField](api:SilverStripe\ORM\FieldType\DBField) comes with its more +Of course that's much more verbose than the equivalent PHP call. 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; -DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago" +// shows "30 years ago" +DBField::create_field('Date', '1982-01-01')->TimeDiff(); ``` -## Casting ViewableData +## Casting `ViewableData` -Most objects in Silverstripe CMS extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view +Most objects in Silverstripe CMS extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view context. Through a `$casting` array, arbitrary properties and getters can be casted: ```php +namespace App\Model; + use SilverStripe\View\ViewableData; -class MyObject extends ViewableData +class MyObject extends ViewableData { - private static $casting = [ - 'MyDate' => 'Date' + 'MyDate' => 'Date', ]; - public function getMyDate() + public function getMyDate() { return '1982-01-01'; } } +``` -$obj = new MyObject; -$obj->getMyDate(); // returns string -$obj->MyDate; // returns string -$obj->obj('MyDate'); // returns object -$obj->obj('MyDate')->InPast(); // returns boolean +```php +use App\Model\MyObject; + +$obj = MyObject::create(); +// returns string +$obj->getMyDate(); +// returns string +$obj->MyDate; +// returns object +$obj->obj('MyDate'); +// returns boolean +$obj->obj('MyDate')->InPast(); ``` -## Casting HTML Text +## 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 fields without the `HTML` prefix automatically in templates, -to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see +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. See the [Template casting](/developer_guides/templates/casting) section for controlling casting in your templates. ## Overloading -"Getters" and "Setters" are functions that 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`. +"Getters" and "Setters" are functions that 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 overload the default behavior by making a function called "get``" or "set``". The following example will use the result of `getStatus` instead of the 'Status' database column. We can refer to the database column using `getField()`. ```php +namespace App\Model; + use SilverStripe\ORM\DataObject; /** @@ -231,10 +254,10 @@ use SilverStripe\ORM\DataObject; */ 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() @@ -246,11 +269,10 @@ class Product extends DataObject { return $this->setField('Cost', $value * 100); } - } ``` -## 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 6d60febeb..d00079cf1 100644 --- a/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md +++ b/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md @@ -3,92 +3,96 @@ 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)s like [Member](api:SilverStripe\Security\Member) without hacking core code or sub -classing by using [DataExtension](api:SilverStripe\ORM\DataExtension). See the [Extending SilverStripe](../extending) guide for more information on +You can add properties and methods to existing [DataObject](api:SilverStripe\ORM\DataObject)s like [Member](api:SilverStripe\Security\Member) without hacking core code or sub +classing by using [DataExtension](api:SilverStripe\ORM\DataExtension). See the [Extending Silverstripe](../extending) guide for more information on [DataExtension](api:SilverStripe\ORM\DataExtension). The following documentation outlines some common hooks that the [Extension](api:SilverStripe\Core\Extension) API provides specifically for managing data records. -## 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()* to save the object to the database. This includes saving a page in the CMS or altering +You can customise saving-behavior for each DataObject, e.g. for adding workflow or data customization. The function is +triggered when calling `write()` to save the object to the database. This includes saving a page in the CMS or altering a `ModelAdmin` record. Example: Disallow creation of new players if the currently logged-in player is not a team-manager. ```php -use SilverStripe\Security\Security; +namespace App\Model; + +use Exception; use SilverStripe\ORM\DataObject; +use SilverStripe\Security\Security; -class Player extends DataObject +class Player extends DataObject { private static $has_many = [ 'Teams' => 'Team', ]; - - public function onBeforeWrite() + + public function onBeforeWrite() { // check on first write action, aka "database row creation" (ID-property is not set) if (!$this->isInDb()) { $currentPlayer = Security::getCurrentUser(); - + if (!$currentPlayer->IsTeamManager()) { - throw new \Exception('Player-creation not allowed'); + throw new Exception('Player-creation not allowed'); } } - + // check on every write action if (!$this->record['TeamID']) { - throw new \Exception('Cannot save player without a valid team'); + throw new Exception('Cannot save player without a valid team'); } - + // CAUTION: You are required to call the parent-function, otherwise // SilverStripe will not execute the request. parent::onBeforeWrite(); } } - ``` -## onBeforeDelete +## `onBeforeDelete` Triggered before executing *delete()* on an existing object. -Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a +Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a member is logged in who belongs to a group containing the permission "PLAYER_DELETE". ```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; -use SilverStripe\ORM\DataObject; -class Player extends DataObject +class Player extends DataObject { - private static $has_many = [ - "Teams" => "Team" + 'Teams' => 'Team', ]; - - public function onBeforeDelete() + + public function onBeforeDelete() { - if(!Permission::check('PLAYER_DELETE')) { + if (!Permission::check('PLAYER_DELETE')) { Security::permissionFailure($this); exit(); } - + parent::onBeforeDelete(); } } ``` [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 fc70be15f..fb8fcc317 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` and `exclude` operations specify exact matches by default. However, when filtering `DataList`s, 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) An example of a `SearchFilter` in use: @@ -24,54 +24,57 @@ An example of a `SearchFilter` in use: // fetch any player that starts with a S $players = Player::get()->filter([ 'FirstName:StartsWith' => 'S', - 'PlayerNumber:GreaterThan' => '10' + 'PlayerNumber:GreaterThan' => '10', ]); // to fetch any player that's name contains the letter 'z' $players = Player::get()->filterAny([ 'FirstName:PartialMatch' => 'z', - 'LastName:PartialMatch' => 'z' + 'LastName:PartialMatch' => 'z', ]); ``` Developers can define their own [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) if needing to extend the ORM filter and exclude behaviors. -These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"` and +These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"` and `":case"`. These negate the filter, make it case-insensitive and make it case-sensitive, respectively. The default -comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is +comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is case-sensitive. ```php // Fetch players that their FirstName is '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 'Sam' (force case-sensitive) $players = Player::get()->filter([ - 'FirstName:ExactMatch:case' => 'Sam' + 'FirstName:ExactMatch:case' => 'Sam', ]); // Fetch players that their FirstName is 'Sam' (force NOT case-sensitive) $players = Player::get()->filter([ - 'FirstName:ExactMatch:nocase' => 'Sam' + 'FirstName:ExactMatch:nocase' => 'Sam', ]); ``` By default the `:ExactMatch` filter is applied, hence why 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 @@ -81,19 +84,20 @@ 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 perform a converse operation to filter anything but a 'W' $players = Player::get()->filter([ - 'FirstName:StartsWith:not' => 'W' + 'FirstName:StartsWith:not' => 'W', ]); ``` -## 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 9f07c7159..e96724db1 100644 --- a/en/02_Developer_Guides/00_Model/07_Permissions.md +++ b/en/02_Developer_Guides/00_Model/07_Permissions.md @@ -4,43 +4,45 @@ 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()`. -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] -By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission +By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission code. [/notice] ```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); } @@ -48,12 +50,12 @@ class MyDataObject extends DataObject ``` [alert] -These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being -checked in the invoking code. The CMS default sections as well as custom interfaces like [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) or +These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being +checked in the invoking code. The CMS default sections as well as custom interfaces like [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) or [GridField](api:SilverStripe\Forms\GridField\GridField) already enforce these permissions. [/alert] -## 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 bc0a53e62..72bff5134 100644 --- a/en/02_Developer_Guides/00_Model/08_SQL_Select.md +++ b/en/02_Developer_Guides/00_Model/08_SQL_Select.md @@ -8,12 +8,12 @@ iconBrand: searchengin ## Introduction -An object representing a SQL select query, which can be serialized into a SQL statement. -It is easier to deal with object-wrappers than string-parsing a raw SQL-query. +An object representing a SQL select query, which can be serialized into a SQL statement. +It is easier to deal with object-wrappers than string-parsing a raw SQL-query. This object is used by the Silverstripe CMS ORM internally. Dealing with low-level SQL is not encouraged, since the ORM provides -powerful abstraction APIs (see [datamodel](/developer_guides/model/data_model_and_orm)). +powerful abstraction APIs (see [datamodel](/developer_guides/model/data_model_and_orm)). Starting with Silverstripe CMS 3, records in collections are lazy loaded, and these collections have the ability to run efficient SQL such as counts or returning a single column. @@ -37,16 +37,16 @@ $count = $query->setFrom('Member')->setSelect('COUNT(*)')->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 can differ from database column) -* DataObject hooks like `onBeforeWrite()` and `onBeforeDelete()` -* Automatic casting -* Default values set through objects -* Database abstraction +- Custom getters/setters (object property can differ from database column) +- DataObject hooks like `onBeforeWrite()` and `onBeforeDelete()` +- Automatic casting +- Default values set through objects +- Database abstraction -We'll explain some ways to use *SELECT* with the full power of SQL, +We'll explain some ways to use `SELECT` with the full power of SQL, but still maintain a connection to the ORM where possible. [warning] @@ -62,14 +62,14 @@ Selection can be done by creating an instance of `SQLSelect`, which allows management of all elements of a SQL SELECT query, including columns, joined tables, conditional filters, grouping, limiting, and sorting. -E.g.: +For example: ```php $sqlQuery = new SQLSelect(); $sqlQuery->setFrom('Player'); $sqlQuery->selectField('FieldName', 'Name'); $sqlQuery->selectField('YEAR("Birthday")', 'Birthyear'); -$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"'); +$sqlQuery->addLeftJoin('Team', '"Player"."TeamID" = "Team"."ID"'); $sqlQuery->addWhere(['YEAR("Birthday") = ?' => 1982]); // $sqlQuery->setOrderBy(...); // $sqlQuery->setGroupBy(...); @@ -84,12 +84,12 @@ $rawSQL = $sqlQuery->sql($parameters); $result = $sqlQuery->execute(); // Iterate over results -foreach($result as $row) { +foreach ($result as $row) { echo $row['BirthYear']; } ``` -The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of [Query](api:SilverStripe\ORM\Connect\Query). +The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of [Query](api:SilverStripe\ORM\Connect\Query). This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data. ### DELETE @@ -143,34 +143,35 @@ Each of these classes implement the interface `SQLWriteExpression`, noting that accepts write key/value pairs in a number of similar ways. These include the following API methods: - * `addAssignments` - Takes a list of assignments as an associative array of key -> value pairs, +- `addAssignments` - Takes a list of assignments as an associative array of key -> value pairs, but also supports SQL expressions as values if necessary - * `setAssignments` - Replaces all existing assignments with the specified list - * `getAssignments` - Returns all currently given assignments, as an associative array +- `setAssignments` - Replaces all existing assignments with the specified list +- `getAssignments` - Returns all currently given assignments, as an associative array in the format `['Column' => ['SQL' => ['parameters]]]` - * `assign` - Singular form of addAssignments, but only assigns a single column value - * `assignSQL` - Assigns a column the value of a specified SQL expression without parameters +- `assign` - Singular form of addAssignments, but only assigns a single column value +- `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` - Clears all assignments - * `getTable` - Gets the table to update - * `setTable` - Sets the table to update (this should be ANSI-quoted) +- `clear` - Clears all assignments +- `getTable` - Gets the table to update +- `setTable` - Sets the table to update (this should be ANSI-quoted) e.g. `$query->setTable('"SiteTree"');` SQLInsert also includes the following API methods: - * `clear` - Clears all rows - * `clearRow` - Clears all assignments on the current row - * `addRow` - Adds another row of assignments, and sets the current row to the new row - * `addRows` - Adds a number of arrays, each representing a list of assignment rows, + +- `clear` - Clears all rows +- `clearRow` - Clears all assignments on the current row +- `addRow` - Adds another row of assignments, and sets the current row to the new row +- `addRows` - Adds a number of arrays, each representing a list of assignment rows, and sets the current row to the last one - * `getColumns` - Gets the names of all distinct columns assigned - * `getInto` - Gets the table to insert into - * `setInto` - Sets the table to insert into (this should be ANSI-quoted), +- `getColumns` - Gets the names of all distinct columns assigned +- `getInto` - Gets the table to insert into +- `setInto` - Sets the table to insert into (this should be ANSI-quoted), e.g. `$query->setInto('"SiteTree"');` -E.g.: +For example: ```php use SilverStripe\ORM\Queries\SQLUpdate; @@ -180,7 +181,7 @@ $update = SQLUpdate::create('"SiteTree"')->addWhere(['ID' => 3]); // assigning a list of items $update->addAssignments([ '"Title"' => 'Our Products', - '"MenuTitle"' => 'Products' + '"MenuTitle"' => 'Products', ]); // Assigning a single value @@ -190,7 +191,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 @@ -200,7 +201,7 @@ $update->assignSQL('"Date"', 'NOW()'); $update->execute(); ``` -In addition to assigning values, the SQLInsert object also supports multi-row +In addition to assigning values, the SQLInsert object also supports multi-row inserts. For database connectors and API that don't have multi-row insert support these are translated internally as multiple single row inserts. @@ -211,11 +212,11 @@ use SilverStripe\ORM\Queries\SQLInsert; $insert = SQLInsert::create('"SiteTree"'); -// Add multiple rows in a single call. Note that column names do not need +// 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 @@ -230,10 +231,9 @@ $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. @@ -256,14 +256,14 @@ $count = $myTeam->Players()->count(); ### 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\ORM\Queries\SQLSelect; use SilverStripe\Forms\DropdownField; +use SilverStripe\ORM\Queries\SQLSelect; $sqlQuery = new SQLSelect(); $sqlQuery->setFrom('Player'); @@ -273,23 +273,32 @@ $map = $sqlQuery->execute()->map(); $field = new DropdownField('Birthdates', 'Birthdates', $map); ``` -Note that going through SQLSelect is just necessary here -because of the custom SQL value transformation (`YEAR()`). +Note that going through SQLSelect is just necessary here +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', ]; - function getNameWithBirthyear() { + + public function getNameWithBirthyear() + { return date('y', $this->Birthdate); } } +``` + +```php +use App\Model\Player; + $players = Player::get(); $map = $players->map('Name', 'NameWithBirthyear'); ``` @@ -298,26 +307,26 @@ $map = $players->map('Name', 'NameWithBirthyear'); As of Silverstripe CMS 4.4, the following PHP types will be 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 Up until Silverstripe CMS 4.3, bugs meant that strings were used for every column type. -## 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 6ac23e9bd..4f25442aa 100644 --- a/en/02_Developer_Guides/00_Model/09_Validation.md +++ b/en/02_Developer_Guides/00_Model/09_Validation.md @@ -4,42 +4,43 @@ 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 on the controller through [form validation](../forms). -While this is a useful approach, it can lead to data inconsistencies if the record is modified outside of the +While this is a useful approach, it can lead to data inconsistencies if the record is modified outside of the controller and form context. -Most validation constraints are actually data constraints which belong on the model. Silverstripe CMS provides the +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. -By default, there is no validation - objects are always valid! However, you can overload this method in your DataObject +By default, there is no validation - objects are always valid! However, you can overload this method in your DataObject sub-classes to specify custom validation, or use the `validate` hook through a [DataExtension](api:SilverStripe\ORM\DataExtension). Invalid objects won't be able to be written - a [ValidationException](api:SilverStripe\ORM\ValidationException) will be thrown and no write will occur. -It is expected that you call `validate()` in your own application to test that an object is valid before attempting a +It is expected that you call `validate()` in your own application to test that an object is valid before attempting a 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 64ed1c6b7..4cf32db89 100644 --- a/en/02_Developer_Guides/00_Model/10_Versioning.md +++ b/en/02_Developer_Guides/00_Model/10_Versioning.md @@ -17,16 +17,15 @@ Versioning in Silverstripe CMS is handled through the [Versioned](api:SilverStri 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. -![](../../_images/snapshot-admin.png) - -*Example screenshot from versioned-snapshot-admin* +![Example screenshot from versioned-snapshot-admin](../../_images/snapshot-admin.png) [/notice] ## Understanding versioning concepts @@ -35,17 +34,18 @@ 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 working off a draft version. Silverstripe CMS handles this through the concept of _stage_. +In most cases, you'll want to have one polished version of a `Page` visible to the general public while your editors might be working off a draft version. Silverstripe CMS handles this through the concept of *stage*. By default, adding the `Versioned` extension to a DataObject will create 2 stages: -* "Stage" for tracking draft content -* "Live" for tracking content publicly visible. + +- "Stage" for tracking draft content +- "Live" for tracking content publicly visible. Publishing a versioned `DataObject` is equivalent to copying the version from the "Stage" stage to the "Live" stage. You can disable stages if your DataObject doesn't require a published version. This will allow you to keep track of all changes that have been applied to a DataObject and who made them. -### 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. @@ -61,7 +61,7 @@ and has_one/has_many, however it relies on a pre-existing relationship to functi 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. @@ -75,7 +75,7 @@ on the owner, but not on the owned object. An unversioned object can own other versioned object. An unversioned object can be configured to automatically publish children 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_ object 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* object 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 @@ -83,7 +83,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. @@ -110,16 +110,17 @@ 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` ```php -extending and the Configuration documentation. +[extending](/developer_guides/extending/) and the [configuration](/developer_guides/configuration/) documentation. [/notice] [warning] @@ -156,52 +158,60 @@ 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] +### Defining ownership between related versioned `DataObject` models -### Defining ownership between related versioned DataObjects - -You can use the `owns` static private 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` static private 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 - 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 = [ 'Parent' => MyPage::class, 'Image' => Image::class, ]; + private static $owns = [ - 'Image' + 'Image', ]; } ``` If a `MyPage` gets published, all its related `Banners` will also be published, which will cause all `Image` DataObjects to be published. -Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`. +Note that ownership cannot be used with polymorphic relations. For example has_one to non-type specific `DataObject`. -#### Unversioned DataObject ownership - -*Requires Silverstripe CMS 4.1 or newer* +#### 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 @@ -214,7 +224,7 @@ The `owns` feature works the same regardless of whether these objects are versio versioned or unversioned dataobjects. You only need to call `->publishRecursive()` on the top most object in the tree. -#### 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 @@ -225,34 +235,47 @@ 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 -first(); @@ -269,18 +292,21 @@ 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. -### Controlling how CMS users interact with versioned DataObjects +### Controlling how CMS users interact with versioned `DataObject` records By default the versioned module includes a `VersionedGridfieldDetailForm` that extends gridfield with versioning support for DataObjects. You can disable this on a per-model basis using the following code: ```php -getComponentByType(GridFieldDetailForm::class) - ->setItemRequestClass(VersionedGridFieldItemRequest::class); - $gridField = GridField::create('Items', 'Items', $this->Items(), $config); - $fields->addFieldToTab('Root.Items', $gridField); - return $fields; + 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. @@ -320,7 +349,6 @@ By default, all records are retrieved from the "Draft" stage (so the `MyRecord` explicitly request a certain stage through various getters on the `Versioned` class. ```php -_versions` tables. ```php -, ); +$historicalRecord = Versioned::get_version('MyRecord', $recordId, $versionId); ``` [alert] @@ -353,47 +380,53 @@ objects, which expose the same database information as a `DataObject`, but also a record was published. ```php -byID(99); // stage doesn't matter here +// stage doesn't matter here +$record = MyRecord::get()->byID(99); $versions = $record->allVersions(); -echo $versions->First()->Version; // instance of Versioned_Version +// instance of Versioned_Version +echo $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, this will transparently create a new version of this DataObject in the _Stage_ stage. +When you call the `write()` method on a versioned `DataObject`, this will transparently create a new version of this `DataObject` in the *Stage* stage. To write your changes without creating new version, call [writeWithoutVersion()](api:SilverStripe\Versioned\Versioned::writeWithoutVersion()) instead. + ```php -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. +$record->Title = 'Foo Bar'; +// This will create a new version of record ID 99. +$record->write(); +// Will output 14. +echo $record->Version; -$record->Title = "FOO BAR"; +$record->Title = 'FOO BAR'; $record->writeWithoutVersion(); -echo $record->Version; // Will still output 14. +// Will still output 14. +echo $record->Version; ``` Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage. -### Publishing a versioned DataObject +### Publishing a versioned `DataObject` -There's two main methods used to publish a versioned DataObject: -* `publishSingle()` publishes this record to live from the draft -* `publishRecursive()` publishes this record, and any dependent objects this record may refer to. +There's two main methods used to publish a versioned `DataObject`: + +- `publishSingle()` publishes this record to live from the draft +- `publishRecursive()` publishes this record, and any dependent objects this record may refer to. In most regular cases, you'll want to use `publishRecursive`. -`publishRecursive` can be called on unversioned DataObject as well if they implement the `RecursivePublishable` extension. +`publishRecursive` can be called on unversioned `DataObject` as well if they implement the `RecursivePublishable` extension. ```php -byID(99); $record->MyField = 'changed'; @@ -404,14 +437,13 @@ $record->write(); $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*. -Use `doUnpublish()` to unpublish an item. Simply call `delete()` to archive an item. The SilverStripe ORM doesn't allow you to _hard-delete_ versioned DataObjects. +Use `doUnpublish()` to unpublish an item. Simply call `delete()` to archive an item. The Silverstripe ORM doesn't allow you to *hard-delete* versioned DataObjects. ```php -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. @@ -425,12 +457,12 @@ $record->delete(); Note that `doUnpublish()` and `doArchive()` do not work recursively. If you wish to unpublish or archive dependants records, you have to do it manually. ### Rolling back to an older version + Rolling back allows you to return a DataObject to a previous state. You can rollback a single DataObject using the `rollbackSingle()` method. You can also rollback all dependent records using the `rollbackRecursive()` method. Both `rollbackSingle()` and `rollbackRecursive()` expect a single argument, which may be a specific version ID or a stage name. ```php -byID(99); @@ -448,64 +480,62 @@ $record->publishRecursive(); ``` Note that internally, rolling back a DataObject creates a new version identical of the restored version ID. For example, -if the live version of `$record` is 10 and the staged version is 13, rolling back to live will create a version 14 in _Stage_ that is identical to version 10. +if the live version of `$record` is 10 and the staged version is 13, rolling back to live will create a version 14 in *Stage* that is identical to version 10. ### Restoring an archived version -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. +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 -isArchived()) { $myRecord->write(); } } ``` -## 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 +### How versioned `DataObject` records are tracked 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 @@ -516,21 +546,22 @@ Note that the "Stage" naming has a special meaning here, it will leave the origi adding a suffix. [/notice] - * `MyRecord` table: Contains staged data - * `MyRecord_Live` table: Contains live data - * `MyRecord_Versions` table: Contains a version history (new record created on each save) +- `MyRecord` table: Contains staged data +- `MyRecord_Live` table: Contains live data +- `MyRecord_Versions` table: Contains a version history (new record created on each save) Similarly, any subclass you create on top of a versioned base will trigger the creation of additional tables, which are automatically joined as required: - * `MyRecordSubclass` table: Contains only staged data for subclass columns - * `MyRecordSubclass_Live` table: Contains only live data for subclass columns - * `MyRecordSubclass_Versions` table: Contains only version history for subclass columns +- `MyRecordSubclass` table: Contains only staged data for subclass columns +- `MyRecordSubclass_Live` table: Contains only live data for subclass columns +- `MyRecordSubclass_Versions` table: Contains only version history for subclass columns Because `many_many` relationships create their own sets of records on their own tables, representing content changes to a DataObject, they can therefore be versioned. This is done using the ["through" setting](https://docs.silverstripe.org/en/4/developer_guides/model/relations/#many-many-through-relationship-joined-on-a-separate-dataobject) 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 - 'Category', ], ]; + // ... } ``` ```php - Product::class, - 'Category'=> Category::class, + 'Category' => Category::class, ]; private static $extensions = [ Versioned::class, ]; + // ... } ``` -### 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. @@ -580,7 +614,6 @@ multiple tables across an inherited table scheme (see [Versioned::augmentSQL()]( Example: Get the first 10 live records, filtered by creation date: ```php -limit(10)->sort('Created', 'ASC'); ``` @@ -602,20 +635,22 @@ You can opt for a session base stage setting through the `Versioned.use_session` Warning: This can lead to leaking of unpublished information, if a live URL is viewed in draft mode, and the result is cached due to aggressive cache settings (not varying on cookie values). -*app/src/MyObject.php* - ```php -ID, '?rand=' . rand()); - $this->extend('updateLink', $link); // updates $link by reference + // updates $link by reference + $this->extend('updateLink', $link); return $link; } @@ -637,15 +673,18 @@ class MyObject extends DataObject { } ``` -*app/src/MyObjectController.php* - ```php -byID($request->param('ID')); @@ -658,27 +697,14 @@ class MyObjectController extends Controller return $html; } - - public function Link($action = null) - { - // Construct link with graceful handling of GET parameters - $link = Controller::join_links('my-objects', $action); - - // Allow Versioned and other extension to update $link by reference. - // Calls VersionedStateExtension->updateLink(). - $this->extend('updateLink', $link, $action); - - return $link; - } } ``` -*app/_config/routes.yml* - -```yaml +```yml +# app/_config/routes.yml SilverStripe\Control\Director: rules: - 'my-objects/$ID': 'MyObjectController' + 'my-objects/$ID': 'App\Control\MyObjectController' ``` [alert] @@ -687,7 +713,7 @@ authenticated to view it. As with any other controller logic, please use `DataOb permissions, and avoid exposing unpublished content to your users. [/alert] -### Controlling permissions to versioned DataObjects +### Controlling permissions to versioned `DataObject` models 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. @@ -698,18 +724,18 @@ 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 +#### Version specific *can* methods + +Versioned `DataObject` records get additional permission check methods to verify what operation a `Member` is allowed to perform: -Versioned DataObjects get additional permission check methods to verify what operation a Member is allowed to perform: -* `canPublish()` -* `canUnpublish()` -* `canArchive()` -* `canViewVersioned()`. +- `canPublish()` +- `canUnpublish()` +- `canArchive()` +- `canViewVersioned()`. -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, +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, ```php -byID(99); @@ -717,13 +743,11 @@ $member = Security::getCurrentUser(); if ($record->canPublish($member)) { $record->publishRecursive(); } - ``` There's also a `canViewStage()` method which can be used to check if a Member can access a specific stage. ```php -canViewStage(Versioned::DRAFT, $member); $record->canViewStage(); ``` -#### Customising permissions for a versioned DataObject +#### Customising permissions for a versioned `DataObject` `Versioned` object visibility can be customised in one of the following ways by editing your user code: - * Override the `canViewVersioned` method in your code. Make sure that this returns true or +- Override the `canViewVersioned` method in your code. 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. +- Override the `canView` method to override the method visibility completely. -E.g. +For example: ```php -getSourceQueryParam("Versioned.mode"); - $stage = $this->getSourceQueryParam("Versioned.stage"); + $mode = $this->getSourceQueryParam('Versioned.mode'); + $stage = $this->getSourceQueryParam('Versioned.stage'); if ($mode === 'Stage' && $stage === 'Live') { return true; } @@ -777,21 +803,24 @@ class MyObject extends DataObject If you want to control permissions of an object in an extension, you can also use one of the below extension points in your `DataExtension` subclass: - * `canView` to update the visibility of the object's `canView` - * `canViewNonLive` to update the visibility of this object only in non-live mode. +- `canView` to update the visibility of the object's `canView` +- `canViewNonLive` to update the visibility of this object only in non-live mode. Note that unlike canViewVersioned, the canViewNonLive method will only be invoked if the object is in a non-published state. -E.g. +For example: ```php -init()` method for this purpose, for example: -**app/src/MyController.php** ```php -public function init() +// app/src/Control/MyController.php +namespace App\Control; + +use SilverStripe\Control\Controller; + +class MyController extends Controller { - parent::init(); - Versioned::set_stage(Versioned::DRAFT); + // ... + + public function init() + { + parent::init(); + Versioned::set_stage(Versioned::DRAFT); + } } ``` @@ -851,12 +892,16 @@ To move a saved version from one stage to another, call [writeToStage(stage)](ap 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 - { 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: @@ -1386,14 +1422,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 0aa91e54d..8f04f7daa 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. ```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; } } @@ -40,31 +42,42 @@ class MyDataObject extends DataObject 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) + ) ) - ) - ); - - return $fields; + ); + + return $fields; + } } ``` -You can also alter the fields of built-in and module `DataObject` classes through your own +You can also alter the fields of built-in and module `DataObject` classes through your own [DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`. [info] `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. @@ -74,19 +87,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', + ]; } ``` @@ -96,9 +110,11 @@ Tabular views such as `GridField` or `ModalAdmin` include a search bar. As of Si #### 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 @@ -106,7 +122,7 @@ class MyDataObject extends DataObject private static $searchable_fields = [ 'Name', 'BirthDate' => [ - 'general' => false + 'general' => false, ], ]; } @@ -120,14 +136,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, which was the default behaviour prior to Silverstripe CMS 4.12. [/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 @@ -142,9 +162,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: @@ -155,8 +176,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; @@ -167,28 +191,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; @@ -197,16 +226,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 @@ -216,17 +252,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` @@ -241,18 +291,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, ]; } ``` @@ -261,11 +314,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', @@ -275,7 +332,7 @@ class MyDataObject extends DataObject 'field' => NumericField::class, 'filter' => 'PartialMatchFilter', ], - ]; + ]; } ``` @@ -284,36 +341,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 @@ -325,6 +389,10 @@ If you don't specify a field, you must use the name of a real database field ins [/alert] ```php +namespace App\Model; + +use SilverStripe\Forms\TextField; + class Order extends DataObject { private static $db = [ @@ -341,22 +409,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 @@ -365,8 +436,8 @@ class MyDataObject extends DataObject 'Name' => 'Text', 'OtherProperty' => 'Text', 'ProductCode' => 'Int', - ]; - + ]; + private static $summary_fields = [ 'Name', 'ProductCode', @@ -379,14 +450,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 { @@ -394,18 +473,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 @@ -413,24 +491,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 @@ -438,23 +517,25 @@ class MyDataObject extends DataObject In order to re-label any summary fields, you can use the `$field_labels` static. This will also affect the output of `$object->fieldLabels()` and `$object->fieldLabel()`. ```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', 'HeroImage.CMSThumbnail', ]; - + private static $field_labels = [ 'Name' => 'Name', 'HeroImage.CMSThumbnail' => 'Hero', @@ -462,11 +543,11 @@ class MyDataObject extends DataObject } ``` -## 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 5a79c9712..d3c4efc6f 100644 --- a/en/02_Developer_Guides/00_Model/12_Indexes.md +++ b/en/02_Developer_Guides/00_Model/12_Indexes.md @@ -5,37 +5,43 @@ 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). ## Defining an index -Indexes are represented on a `DataObject` through the `DataObject::$indexes` array which maps index names to a + +Indexes are represented on a `DataObject` through the `DataObject::$indexes` array 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' => ['', ''], ], '' => ['', ''], @@ -43,23 +49,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', @@ -77,28 +84,31 @@ Please note that if you have previously used the removed `value` key to define a now throw an error. Use `columns` instead. [/alert] -## 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. For example -*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. As of 3.7.0 `default_sort` fields will automatically become database indexes as this provides significant performance benefits. -## 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 c00c5c1a0..f84b23585 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 e0b8b572e..43b950c5a 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,28 +3,32 @@ title: Dynamic Default Fields summary: Learn how to add default values to your models --- -# Default Values and Records +# Default values and records + +## 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 overloaded. @@ -35,13 +39,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(); + } } ``` @@ -49,34 +62,50 @@ It's also possible to get the data from any other source, or another object, jus methods. For example: ```php -/** - * This method combines the Title of the parent object with the Title of this - * object in the FullTitle field. - */ -public function populateDefaults() +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class Dog extends DataObject { - if($parent = $this->Parent()) { - $this->FullTitle = $parent->Title . ': ' . $this->Title; - } else { - $this->FullTitle = $this->Title; + // ... + + /** + * This method combines the Title of the parent object with the Title of this + * object in the FullTitle field. + */ + public function populateDefaults() + { + $parent = $this->Parent() + + if ($parent && $parent->exists()) { + $this->FullTitle = $parent->Title . ': ' . $this->Title; + } else { + $this->FullTitle = $this->Title; + } + + parent::populateDefaults(); } - parent::populateDefaults(); } ``` -## 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'], 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 b47dbb0ad..0e179ba5e 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 @@ -10,69 +10,76 @@ These lists can get quite long, and hard to present on a single list. [Pagination](/developer_guides/templates/how_tos/pagination) is one way to solve this problem, by splitting up the list into multiple pages. -In this howto, we present an alternative to pagination: +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, -adding new functionality. +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. 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')); } @@ -80,7 +87,7 @@ class Page extends SiteTree ``` 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 @@ -96,11 +103,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, @@ -109,40 +116,46 @@ 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')); } } ``` + The final step is to render this into the template using the [GroupedList::GroupedBy()](api:SilverStripe\ORM\GroupedList::GroupedBy()) method. ```ss @@ -160,4 +173,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 e828140f8..b7121fa82 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 will be represented by a [DataObject](api:SilverStripe\ORM\DataObject) class. 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, CMS user or mail subscriber. [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 a811df6e9..9bc72f416 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..) 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 @@ processed through [SSViewer](api:SilverStripe\View\SSViewer). This process repla An example of a Silverstripe CMS template is below: -**app/templates/Page.ss** - ```ss +<%-- app/templates/Page.ss --%> <% base_tag %> @@ -44,7 +43,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] @@ -56,7 +55,7 @@ a module, theme, or your `app/` folder. By default, templates will have the same name as the class they are used to render. So, your `Page` class will be rendered with the `templates/Page.ss` template. -When the class has a namespace, the namespace will be interpreted as a subfolder within the `templates` path. +When the class has a namespace, the namespace will be interpreted as a subfolder within the `templates` path. For example, the class `SilverStripe\Control\Controller` will be rendered with the `templates/SilverStripe/Control/Controller.ss` template. @@ -66,15 +65,15 @@ where you need to leave out the `Includes/` folder. ## Variables -Variables are placeholders that will be replaced with data from the [DataModel](../model/) or the current -[Controller](../controllers). Variables are prefixed with a `$` character. Variable names must start with an +Variables are placeholders that will be replaced with data from the [DataModel](../model/) or the current +[Controller](../controllers). Variables are prefixed with a `$` character. Variable names must start with an alphabetic character or underscore, with subsequent characters being alphanumeric or underscore: ```ss $Title ``` -This inserts the value of the Title database field of the page being displayed in place of `$Title`. +This inserts the value of the Title database field of the page being displayed in place of `$Title`. Variables can be chained together, and include arguments. @@ -86,12 +85,12 @@ $Foo.Bar These variables will call a method / field on the object and insert the returned value as a string into the template. -* `$Foo` will call `$obj->Foo()` (or the field `$obj->Foo`) -* `$Foo(param)` will call `$obj->Foo("param")` -* `$Foo.Bar` will call `$obj->Foo()->Bar()` +- `$Foo` will call `$obj->Foo()` (or the field `$obj->Foo`) +- `$Foo(param)` will call `$obj->Foo("param")` +- `$Foo.Bar` will call `$obj->Foo()->Bar()` If a variable returns a string, that string will be inserted into the template. If the variable returns an object, then -the system will attempt to render the object through its `forTemplate()` method. If the `forTemplate()` method has not +the system will attempt to render the object through its `forTemplate()` method. If the `forTemplate()` method has not been defined, the system will return an error. [notice] @@ -99,39 +98,45 @@ If you wish to pass parameters to getter functions, you must use the full method [/notice] [note] -For more detail around how variables are inserted and formatted into a template see +For more detail around how variables are inserted and formatted into a template see [Formatting, Modifying and Casting Variables](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.

``` [note] - Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`. +Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`. [/note] The variables that can be used in a template vary based on the object currently in [scope](#scope). Scope defines what -object the methods get called on. For the standard `Page.ss` template the scope is the current [PageController](api:SilverStripe\CMS\Controllers\ContentController\PageController) +object the methods get called on. For the standard `Page.ss` template the scope is the current [PageController](api:SilverStripe\CMS\Controllers\ContentController\PageController) class. This object gives you access to all the database fields on [PageController](api:SilverStripe\CMS\Model\SiteTree\PageController), its corresponding [Page](api:SilverStripe\CMS\Model\SiteTree\Page) record and any subclasses of those two. -**app/src/Layout/Page.ss** - ```ss +<%-- app/templates/Layout/Page.ss --%> $Title // returns the page `Title` property @@ -139,7 +144,7 @@ $Content // returns the page `Content` property ``` -## Conditional Logic +## Conditional logic The simplest conditional block is to check for the presence of a value (does not equal 0, null, false). @@ -158,7 +163,7 @@ A conditional can also check for a value other than falsy. ``` [notice] -When inside template tags variables should have a '$' prefix, and literals should have quotes. +When inside template tags variables should have a '$' prefix, and literals should have quotes. [/notice] Conditionals can also provide the `else` case. @@ -195,7 +200,6 @@ You can check if a variable is false with `<% if not %>`. Note that you cannot combine this with other operators such as `==`. - For more nuanced check you can use the `!` operator. ```ss @@ -204,9 +208,9 @@ For more nuanced check you can use the `!` operator. <% end_if %> ``` -### Boolean Logic +### Boolean logic -Multiple checks can be done using `||`, `or`, `&&` or `and`. +Multiple checks can be done using `||`, `or`, `&&` or `and`. If *either* of the conditions is true. @@ -252,7 +256,7 @@ When using subfolders in your template structure <% 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 @@ -261,7 +265,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 @@ -270,9 +274,9 @@ include. <% end_with %> ``` -## 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 @@ -284,21 +288,21 @@ 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](#scope). When inside a <% loop %> the scope of the template has changed to the +This demonstrates the concept of [Scope](#scope). When inside a <% loop %> the scope of the template has changed to the object that is being looped over. [/notice] ### Altering the list -`<% loop %>` statements iterate over a [DataList](api:SilverStripe\ORM\DataList) instance. As the template has access to the list object, -templates can call [DataList](api:SilverStripe\ORM\DataList) methods. +`<% loop %>` statements iterate over a [DataList](api:SilverStripe\ORM\DataList) instance. As the template has access to the list object, +templates can call [DataList](api:SilverStripe\ORM\DataList) methods. Sorting the list by a given field. @@ -350,22 +354,22 @@ 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. - * `$Even`, `$Odd`: Returns boolean, handy for zebra striping. - * `$EvenOdd`: Returns a string, either 'even' or 'odd'. Useful for CSS classes. - * `$First`, `$Last`, `$Middle`: Booleans about the position in the list. - * Note: as of CMS 4.7.0 `$IsFirst` and `$IsLast` will be preferred. The original +- `$Even`, `$Odd`: Returns boolean, handy for zebra striping. +- `$EvenOdd`: Returns a string, either 'even' or 'odd'. Useful for CSS classes. +- `$First`, `$Last`, `$Middle`: Booleans about the position in the list. + - Note: as of CMS 4.7.0 `$IsFirst` and `$IsLast` will be preferred. The original syntax will continue to work, but will be deprecated in a future release. - * `$FirstLast`: Returns a string, "first", "last", "first last" (if both), or "". Useful for CSS classes. - * `$Pos`: The current position in the list (integer). +- `$FirstLast`: Returns a string, "first", "last", "first last" (if both), or "". Useful for CSS classes. +- `$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,11 +384,11 @@ iteration. ``` [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. @@ -393,7 +397,7 @@ pagination. $Modulus(value, offset) // returns a boolean. -$MultipleOf(factor, offset) +$MultipleOf(factor, offset) <% loop $Children %>
    @@ -405,11 +409,11 @@ $MultipleOf(factor, offset) ``` [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 $Modulus(3) as a class and add a `clear: both` to `.column-1`. [/hint] -`$MultipleOf(value, offset)` can also be utilized to build column and grid layouts. In this case we want to add a `
    ` +`$MultipleOf(value, offset)` can also be utilized to build column and grid layouts. In this case we want to add a `
    ` after every 3rd item. ```ss @@ -449,21 +453,23 @@ 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, and any [Extension](api:SilverStripe\Core\Extension) instances you have. -### Navigating Scope +### Navigating scope #### Up @@ -482,21 +488,23 @@ When in a particular scope, `$Up` takes the scope back to the previous level. ``` Given the following structure, it will output the text. -``` - My Page - | - +-+ Child 1 - | | - | +- Grandchild 1 - | - +-+ Child 2 - Children of 'My Page' +```text + My Page + | + +-+ Child 1 + | | + | +- Grandchild 1 + | + +-+ Child 2 - Page 'Child 1' is a child of 'My Page' - Page 'Grandchild 1' is a grandchild of 'My Page' - Page 'Child 2' is a child of 'MyPage' + Children of 'My Page' + + Page 'Child 1' is a child of 'My Page' + Page 'Grandchild 1' is a grandchild of 'My Page' + Page 'Child 2' is a child of 'MyPage' ``` + [notice] Additional selectors implicitly change the scope so you need to put additional `$Up` to get what you expect. [/notice] @@ -504,14 +512,14 @@ Additional selectors implicitly change the scope so you need to put additional ` ```ss

    Children of '$Title'

    <% loop $Children.Sort('Title').First %> - <%-- We have two additional selectors in the loop expression so... --%> + <%-- We have two additional selectors in the loop expression so... --%>

    Page '$Title' is a child of '$Up.Up.Up.Title'

    <% end_loop %> ``` #### 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 page. The previous example could be rewritten to use the following syntax. ```ss @@ -544,8 +552,8 @@ 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 +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 `$CurrentMember.FirstName`. ### Me @@ -571,10 +579,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 -## Related Documentation +- [Creating your first theme](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-theme-1) + +## Related documentation [CHILDREN Exclude="How_Tos"] @@ -582,9 +591,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 d2747f91f..a33ae7887 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,8 +29,7 @@ Some of the following only apply when you have the `CMS` module installed. If yo functionality may not be included. [/alert] - -## Base Tag +## Base tag ```ss @@ -42,7 +41,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. +to locate your site’s images and CSS files. It renders in the template as `` @@ -50,7 +49,7 @@ It renders in the template as ` ``` -### Linking Modes +### Linking modes ```ss $isSection @@ -230,9 +238,9 @@ An example for checking for `current` or `section` is as follows: $MenuTitle ``` -**Additional Utility Method** +#### Additional utility method - * `$InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children. +- `$InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children. ```ss <% if $InSection(about-us) %> @@ -240,7 +248,7 @@ An example for checking for `current` or `section` is as follows: <% end_if %> ``` -### URLSegment +### `URLSegment` This returns the part of the URL of the page you're currently on. For example on the `/about-us/offices/` web page the `URLSegment` will be `offices`. `URLSegment` cannot be used to generate a link since it does not output the full path. @@ -254,7 +262,7 @@ It can be used within templates to generate anchors or other CSS classes. ``` -## ClassName +## `ClassName` Returns the class of the current object in [scope](syntax#scope) such as `Page` or `HomePage`. The `$ClassName` can be handy for a number of uses. A common use case is to add to your `` tag to influence CSS styles and JavaScript @@ -266,7 +274,7 @@ behavior based on the page type used: ``` -## Children Loops +## `Children` loops ```ss <% loop $Children %> @@ -282,7 +290,7 @@ For doing your website navigation most likely you'll want to use `$Menu` since i context. [/alert] -### ChildrenOf +### `ChildrenOf` ```ss <% loop $ChildrenOf() %> @@ -294,8 +302,7 @@ Will create a list of the children of the given page, as identified by its `URLS because it's not dependent on the context of the current page. For example, it would allow you to list all staff member pages underneath a "staff" holder on any page, regardless if its on the top level or elsewhere. - -### AllChildren +### `AllChildren` Content authors have the ability to hide pages from menus by un-selecting the `ShowInMenus` checkbox within the CMS. This option will be honored by `<% loop $Children %>` and `<% loop $Menu %>` however if you want to ignore the user @@ -307,7 +314,7 @@ preference, `AllChildren` does not filter by `ShowInMenus`. <% end_loop %> ``` -### Menu Loops +### `Menu` loops ```ss <% loop $Menu(1) %> @@ -321,7 +328,7 @@ preference, `AllChildren` does not filter by `ShowInMenus`. Pages with the `ShowInMenus` property set to `false` will be filtered out. [/notice] -## Access to a specific Page +## Access to a specific page ```ss <% with $Page(my-page) %> @@ -331,9 +338,9 @@ Pages with the `ShowInMenus` property set to `false` will be filtered out. Page will return a single page from site, looking it up by URL. -## Access to Parent and Level Pages +## Access to parent and level pages -### Level +### `Level` ```ss <% with $Level(1) %> @@ -346,11 +353,11 @@ looking back through its parent pages. `Level(1)` being the top most level. For example, imagine you're on the "bob marley" page, which is three levels in: "about us > staff > bob marley". -* `$Level(1).Title` would return "about us" -* `$Level(2).Title` would return "staff" -* `$Level(3).Title` would return "bob marley" +- `$Level(1).Title` would return "about us" +- `$Level(2).Title` would return "staff" +- `$Level(3).Title` would return "bob marley" -### Parent +### `Parent` ```ss @@ -362,11 +369,11 @@ $Parent.Parent.Title ``` -## Navigating Scope +## Navigating scope See [scope](syntax#scope). -## Breadcrumbs +## `Breadcrumbs` Breadcrumbs are the path of pages which need to be taken to reach the current page, and can be a great navigation aid for website users. @@ -391,7 +398,7 @@ of the `silverstripe/cms` module. [info] To customise the markup that `$Breadcrumbs` generates, copy `templates/BreadcrumbsTemplate.ss` - from the `silverstripe/cms` module to your theme (e.g.: `themes/you-theme/templates/BreadcrumbsTemplate.ss`). + from the `silverstripe/cms` module to your theme (e.g: `themes/you-theme/templates/BreadcrumbsTemplate.ss`). Modify the newly copied template and flush your Silverstripe CMS cache. [/info] @@ -405,18 +412,18 @@ A page will normally contain some content and potentially a form of some kind. F Silverstripe CMS log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form. Placing it just below `$Content` is a good default. +## Related lessons -## Related Lessons -* [Adding dynamic content](https://www.silverstripe.org/learn/lessons/v4/adding-dynamic-content-1) +- [Adding dynamic content](https://www.silverstripe.org/learn/lessons/v4/adding-dynamic-content-1) -## Related Documentation +## Related documentation - * [Casting and Formatting Variables](casting) - * [Template Inheritance](template_inheritance) +- [Casting and Formatting Variables](casting) +- [Template Inheritance](template_inheritance) -## API Documentation +## API documentation - * [ContentController](api:SilverStripe\CMS\Controllers\ContentController): The main controller responsible for handling pages. - * [Controller](api:SilverStripe\Control\Controller): Generic controller (not specific to pages.) - * [DataObject](api:SilverStripe\ORM\DataObject): Underlying model class for page objects. - * [ViewableData](api:SilverStripe\View\ViewableData): Underlying object class for pretty much anything displayable. +- [ContentController](api:SilverStripe\CMS\Controllers\ContentController): The main controller responsible for handling pages. +- [Controller](api:SilverStripe\Control\Controller): Generic controller (not specific to pages.) +- [DataObject](api:SilverStripe\ORM\DataObject): Underlying model class for page objects. +- [ViewableData](api:SilverStripe\View\ViewableData): Underlying object class for pretty much anything displayable. diff --git a/en/02_Developer_Guides/01_Templates/03_Requirements.md b/en/02_Developer_Guides/01_Templates/03_Requirements.md index 4ec329c5d..2da80154b 100644 --- a/en/02_Developer_Guides/01_Templates/03_Requirements.md +++ b/en/02_Developer_Guides/01_Templates/03_Requirements.md @@ -6,8 +6,8 @@ iconBrand: js # Requirements -The requirements class takes care of including CSS and JavaScript into your applications. This is preferred to hard -coding any references in the `` tag of your template, as it enables a more flexible handling through the +The requirements class takes care of including CSS and JavaScript into your applications. This is preferred to hard +coding any references in the `` tag of your template, as it enables a more flexible handling through the [Requirements](api:SilverStripe\View\Requirements) class. The examples below are using certain folder naming conventions (CSS files in `css/`, JavaScript files in `javascript/`). @@ -54,18 +54,21 @@ This Composer plugin automatically tries to expose assets from your project and Developers can explicitly expose static assets by calling `composer vendor-expose`. This is necessary after updating your `resources-dir` or `expose` configuration in your `composer.json` file. -`composer vendor-expose` accepts an optional `method` argument (e.g.: `composer vendor-expose auto`). This controls how the files are exposed in the "resources" directory: -* `none` disables all symlink / copy -* `copy` copies the exposed files -* `symlink` create symbolic links to the exposed folder -* `junction` uses a junction (Windows only) -* `auto` creates symbolic links (or junctions on Windows), but fails over to copy. +`composer vendor-expose` accepts an optional `method` argument (e.g: `composer vendor-expose auto`). This controls how the files are exposed in the "resources" directory: + +- `none` disables all symlink / copy +- `copy` copies the exposed files +- `symlink` create symbolic links to the exposed folder +- `junction` uses a junction (Windows only) +- `auto` creates symbolic links (or junctions on Windows), but fails over to copy. ### Referencing exposed assets -When referencing exposed static assets, use either the project file path (relative to the project root folder) or a module name and relative file path to that module's root folder. E.g.: +When referencing exposed static assets, use either the project file path (relative to the project root folder) or a module name and relative file path to that module's root folder. e.g: ```php +use SilverStripe\View\Requirements; + // When referencing project files, use the same path defined in your `composer.json` file. Requirements::javascript('app/client/dist/bundle.js'); @@ -78,11 +81,10 @@ Requirements::javascript('silverstripe/admin:client/dist/js/bundle.js'); When rendered in HTML code, these URLs will be rewritten to their matching path inside the "resources" directory. -## Template Requirements API +## Template requirements API -**/templates/SomeTemplate.ss** - -``` +```ss +<%-- /templates/SomeTemplate.ss --%> <% require css("/css/some_file.css") %> <% require themedCSS("some_themed_file") %> <% require javascript("/javascript/some_file.js") %> @@ -92,13 +94,14 @@ When rendered in HTML code, these URLs will be rewritten to their matching path Requiring assets from the template is restricted compared to the PHP API. [/alert] -## PHP Requirements API +## PHP requirements API It is common practice to include most Requirements either in the *init()*-method of your [controller](../controllers/), or as close to rendering as possible (e.g. in [FormField](api:SilverStripe\Forms\FormField)). ```php -use SilverStripe\Control\Director; +namespace App\Control; + use SilverStripe\View\Requirements; class MyCustomController extends Controller @@ -107,13 +110,13 @@ class MyCustomController extends Controller { parent::init(); - Requirements::javascript("/javascript/some_file.js"); - Requirements::css("/css/some_file.css"); + Requirements::javascript('/javascript/some_file.js'); + Requirements::css('/css/some_file.css'); } } ``` -### CSS Files +### CSS files ```php use SilverStripe\View\Requirements; @@ -121,28 +124,33 @@ use SilverStripe\View\Requirements; Requirements::css($path, $media); ``` -If you're using the CSS method a second argument can be used. This argument defines the 'media' attribute of the +If you're using the CSS method a second argument can be used. This argument defines the 'media' attribute of the `` element, so you can define 'screen' or 'print' for example. ```php -Requirements::css("/css/some_file.css", "screen,projection"); +Requirements::css('/css/some_file.css', 'screen,projection'); ``` -### Javascript Files +### JavaScript files ```php +use SilverStripe\View\Requirements; + Requirements::javascript($path, $options); ``` -A variant on the inclusion of custom javascript is the inclusion of *templated* javascript. Here, you keep your -JavaScript in a separate file and instead load, via search and replace, several PHP-generated variables into that code. +A variant on the inclusion of custom JavaScript is the inclusion of *templated* JavaScript. Here, you keep your +JavaScript in a separate file and instead load, via search and replace, several PHP generated variables into that code. ```php +use SilverStripe\Security\Security; +use SilverStripe\View\Requirements; + $vars = [ - "MemberID" => Security::getCurrentUser()->ID, + 'MemberID' => Security::getCurrentUser()->ID, ]; -Requirements::javascriptTemplate("/javascript/some_file.js", $vars); +Requirements::javascriptTemplate('/javascript/some_file.js', $vars); ``` In this example, `some_file.js` is expected to contain a replaceable variable expressed as `MemberID`. @@ -153,34 +161,41 @@ Requirement calls that rely on those included scripts will not double include th files. ```php +use SilverStripe\View\Requirements; + Requirements::javascript('/javascript/dist/bundle.js', ['provides' => [ '/javascript/jquery.js' '/javascript/src/main.js', - '/javascript/src/functions.js' + '/javascript/src/functions.js', ]]); -Requirements::javascript('/javascript/jquery.js'); // Will skip this file +// Will skip this file +Requirements::javascript('/javascript/jquery.js'); ``` You can also use the second argument to add the 'async' and/or 'defer attributes to the script tag generated: ```php +use SilverStripe\View\Requirements; + Requirements::javascript( - "/javascript/some_file.js", + '/javascript/some_file.js', [ - "async" => true, - "defer" => true, + 'async' => true, + 'defer' => true, ] ); ``` -### Custom Inline CSS or Javascript +### Custom inline CSS or JavaScript You can also quote custom scripts directly. This may seem a bit ugly, but is useful when you need to transfer some kind -of 'configuration' from the database in a raw format. You'll need to use the `heredoc` syntax to quote JS and CSS, +of 'configuration' from the database in a raw format. You'll need to use the `heredoc` syntax to quote JS and CSS, this is generally speaking the best way to do these things - it clearly marks the copy as belonging to a different language. ```php +use SilverStripe\View\Requirements; + Requirements::customScript(<<.js`, @@ -226,29 +243,28 @@ other backend. You can also use any of the below options in order to tweak this behaviour: - * `Requirements.disable_flush_combined` - By default all combined files are deleted on flush. +- `Requirements.disable_flush_combined` - By default all combined files are deleted on flush. If combined files are stored in source control, and thus updated manually, you might want to turn this on to disable this behaviour. - * `Requirements_Backend.combine_hash_querystring` - By default the `` of the source files is appended to +- `Requirements_Backend.combine_hash_querystring` - By default the `` of the source files is appended to the end of the combined file (prior to the file extension). If combined files are versioned in source control, or running in a distributed environment (such as one where the newest version of a file may not always be immediately available) then it may sometimes be necessary to disable this. When this is set to true, the hash will instead be appended via a querystring parameter to enable cache busting, but not in the filename itself. I.e. `assets/_combinedfiles/name.js?m=` - * `Requirements_Backend.default_combined_files_folder` - This defaults to `_combinedfiles`, and is the folder +- `Requirements_Backend.default_combined_files_folder` - This defaults to `_combinedfiles`, and is the folder within the configured asset backend that combined files will be stored in. If using a backend shared with other systems, it is usually necessary to distinguish combined files from other assets. - * `Requirements_Backend.combine_in_dev` - By default combined files will not be combined except in test +- `Requirements_Backend.combine_in_dev` - By default combined files will not be combined except in test or live environments. Turning this on will allow for pre-combining of files in development mode. - * `Requirements_Backend.resolve_relative_css_refs` - Enables rewriting of relative paths to image/font assets - to accommodate the fact that the combined CSS is placed in a totally different folder than the source css +- `Requirements_Backend.resolve_relative_css_refs` - Enables rewriting of relative paths to image/font assets + to accommodate the fact that the combined CSS is placed in a totally different folder than the source CSS files. Disabled by default. In some cases it may be necessary to create a new storage backend for combined files, if the default location is not appropriate. Normally a single backend is used for all site assets, so a number of objects must be replaced. For instance, the below will set a new set of dependencies to write to `app/javascript/combined` - ```yml --- Name: myrequirements @@ -284,7 +300,7 @@ SilverStripe\Core\Injector\Injector: In the above configuration, automatic expiry of generated files has been disabled, and it is necessary for the developer to maintain these files manually. This may be useful in environments where assets must -be pre-cached, where scripts must be served alongside static files, or where no framework php request is +be pre-cached, where scripts must be served alongside static files, or where no framework PHP request is guaranteed. Alternatively, files may be served from instances other than the one which generated the page response, and file synchronisation might not occur fast enough to propagate combined files to mirrored filesystems. @@ -292,21 +308,25 @@ mirrored filesystems. In any case, care should be taken to determine the mechanism appropriate for your development and production environments. -### Combined CSS Files +### Combined CSS files You can also combine CSS files into a media-specific stylesheets as you would with the `Requirements::css` call - use the third parameter of the `combine_files` function: ```php -$loader = SilverStripe\View\ThemeResourceLoader::inst(); -$themes = SilverStripe\View\SSViewer::get_themes(); +use SilverStripe\View\Requirements; +use SilverStripe\View\SSViewer; +use SilverStripe\View\ThemeResourceLoader; + +$loader = ThemeResourceLoader::inst(); +$themes = SSViewer::get_themes(); $printStylesheets = [ $loader->findThemedCSS('print_HomePage.css', $themes), - $loader->findThemedCSS('print_Page.css', $themes) + $loader->findThemedCSS('print_Page.css', $themes), ]; -SilverStripe\View\Requirements::combine_files('print.css', $printStylesheets, 'print'); +Requirements::combine_files('print.css', $printStylesheets, 'print'); ``` By default, all requirements files are flushed (deleted) when ?flush querystring parameter is set. @@ -317,30 +337,34 @@ When combining CSS files, take care of relative urls, as these will not be re-wr the destination location of the resulting combined CSS. [/alert] -### Combined JS Files +### Combined JS files -You can also add the 'async' and/or 'defer' attributes to combined Javascript files as you would with the +You can also add the 'async' and/or 'defer' attributes to combined JavaScript files as you would with the `Requirements::javascript` call - use the third parameter of the `combine_files` function: ```php -$loader = SilverStripe\View\ThemeResourceLoader::inst(); -$themes = SilverStripe\View\SSViewer::get_themes(); +use SilverStripe\View\Requirements; +use SilverStripe\View\SSViewer; +use SilverStripe\View\ThemeResourceLoader; + +$loader = ThemeResourceLoader::inst(); +$themes = SSViewer::get_themes(); $scripts = [ $loader->findThemedJavascript('some_script.js', $themes), - $loader->findThemedJavascript('some_other_script.js', $themes) + $loader->findThemedJavascript('some_other_script.js', $themes), ]; -SilverStripe\View\Requirements::combine_files('scripts.js', $scripts, ['async' => true, 'defer' => true]); +Requirements::combine_files('scripts.js', $scripts, ['async' => true, 'defer' => true]); ``` ### Minification of CSS and JS files -You can minify combined Javascript and CSS files at runtime using an implementation of the +You can minify combined JavaScript and CSS files at runtime using an implementation of the `SilverStripe\View\Requirements_Minifier` interface. ```php -namespace MyProject; +namespace App; use SilverStripe\View\Requirements_Minifier; @@ -353,8 +377,8 @@ class MyMinifier implements Requirements_Minifier * @param string $type Either js or css * @param string $filename Name of file to display in case of error * @return string minified content - */ - public function minify ($content, $type, $fileName) + */ + public function minify($content, $type, $fileName) { // Minify $content; @@ -365,12 +389,12 @@ class MyMinifier implements Requirements_Minifier Then, inject this service in `Requirements_Backend`. -```yaml +```yml SilverStripe\Core\Injector\Injector: SilverStripe\View\Requirements_Backend: properties: MinifyCombinedFiles: true - Minifier: '%$MyProject\MyMinifier' + Minifier: '%$App\MyMinifier' ``` [alert] @@ -378,16 +402,19 @@ While the framework does afford you the option of minification at runtime, we re tools to do this for you, e.g. [Webpack](https://webpack.github.io/), [Gulp](http://gulpjs.com/), or [Grunt](https://gruntjs.com/). [/alert] - ## Clearing assets ```php +use SilverStripe\View\Requirements; + Requirements::clear(); ``` Clears all defined requirements. You can also clear specific requirements. ```php +use SilverStripe\View\Requirements; + Requirements::clear('modulename/javascript/some-lib.js'); ``` @@ -397,58 +424,64 @@ Depending on where you call this command, a Requirement might be *re-included* a ## Blocking -Requirements can also be explicitly blocked from inclusion, which is useful to avoid conflicting JavaScript logic or -CSS rules. These blocking rules are independent of where the `block()` call is made. It applies both for already +Requirements can also be explicitly blocked from inclusion, which is useful to avoid conflicting JavaScript logic or +CSS rules. These blocking rules are independent of where the `block()` call is made. It applies both for already included requirements, and ones included after the `block()` call. -One common example is to block the core `jquery.js` added by various form fields and core controllers, and use a newer +One common example is to block the core `jquery.js` added by various form fields and core controllers, and use a newer version in a custom location. This assumes you have tested your application with the newer version. ```php +use SilverStripe\View\Requirements; + Requirements::block('silverstripe/admin:thirdparty/jquery/jquery.js'); ``` [alert] -The CMS also uses the `Requirements` system, and its operation can be affected by `block()` calls. Avoid this by +The CMS also uses the `Requirements` system, and its operation can be affected by `block()` calls. Avoid this by limiting the scope of your blocking operations, e.g. in `init()` of your controller. [/alert] -## Inclusion Order +## Inclusion order Requirements acts like a stack, where everything is rendered sequentially in the order it was included. There is no way -to change inclusion-order, other than using *Requirements::clear* and rebuilding the whole set of requirements. +to change inclusion-order, other than using *Requirements::clear* and rebuilding the whole set of requirements. [alert] -Inclusion order is both relevant for CSS and Javascript files in terms of dependencies, inheritance and overlays - be +Inclusion order is both relevant for CSS and JavaScript files in terms of dependencies, inheritance and overlays - be careful when messing with the order of requirements. [/alert] -## Javascript placement +## JavaScript placement -By default, Silverstripe CMS includes all Javascript files at the bottom of the page body, unless there's another script +By default, Silverstripe CMS includes all JavaScript files at the bottom of the page body, unless there's another script already loaded, then, it's inserted before the first `