├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── add-on-structure.md ├── controller-basics.md ├── criteria.md ├── css │ └── extra.css ├── designing-styles.md ├── development-tools.md ├── entities-finders-repositories.md ├── files │ ├── Demo-Portal-1.0.0 Alpha.zip │ ├── example-sources │ │ ├── all-for-one-criterion-2.0.10.zip │ │ └── posts-remover-2.0.10.zip │ ├── images │ │ ├── example-custom-criteria-awarded.png │ │ ├── example-custom-criteria-notice.png │ │ ├── example-custom-criteria-type-messages-after.png │ │ ├── example-custom-criteria-type-messages-before.png │ │ ├── example-custom-criteria-type-remover.png │ │ ├── example-userbanners-tag.png │ │ ├── helper_criteria_tabs_example.png │ │ ├── linux-debugging.jpg │ │ ├── linux-php-versions.png │ │ ├── macos-debugging.jpg │ │ ├── macos-php-versions.png │ │ └── server-report.png │ ├── info.zip │ ├── linux │ │ ├── install-debian.sh │ │ ├── install-ubuntu.sh │ │ ├── php56 │ │ │ ├── x.conf │ │ │ └── xdebug.ini │ │ ├── php74 │ │ │ ├── x.conf │ │ │ └── xdebug.ini │ │ └── php80 │ │ │ ├── x.conf │ │ │ └── xdebug.ini │ ├── macos │ │ ├── httpd │ │ │ └── httpd-dev.conf │ │ ├── php56 │ │ │ ├── htaccess.txt │ │ │ ├── php-dev.ini │ │ │ └── x.conf │ │ ├── php74 │ │ │ ├── htaccess.txt │ │ │ ├── php-dev.ini │ │ │ └── x.conf │ │ └── php80 │ │ │ ├── htaccess.txt │ │ │ ├── php-dev.ini │ │ │ └── x.conf │ ├── scotchbox │ │ └── Vagrantfile │ └── scotchboxpro │ │ └── Vagrantfile ├── general-concepts.md ├── images │ ├── favicon.png │ └── logo.svg ├── index.md ├── js │ └── extra.js ├── lets-build-an-add-on.md ├── linux-dev.md ├── macos-dev.md ├── managing-the-schema.md ├── rest-api.md ├── routing-basics.md ├── template-syntax.md ├── vscode.md └── windows-dev.md ├── mkdocs.yml ├── overrides ├── main.html └── partials │ └── logo.html ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | site/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing documentation changes 2 | 3 | If you're reading this, **THANK YOU** for considering helping us improve and expand our developer documentation 👍 4 | 5 | There are three important things to note about documentation, generally: 6 | 7 | 1. It's the **best** thing ever. We already have a massive pool of talent within the [XenForo community](https://xenforo.com/community) and a considerable number of them got to where they are today with almost no documentation at all! Not everyone can learn a new code language / framework in this way, and so this documentation is important so the massive pool of talent only gets bigger and better. 8 | 2. It's the **worst** thing ever. At least for some people. Some developers **hate** writing documentation. It's time consuming and not easy. 9 | 3. It's a **rewarding** and **admirable** task to be able to impart our own knowledge onto others. This is the most important bit, so refer back to #1 😉 10 | 11 | These guidelines aim to set out some of the processes involved in editing our documentation, and some best practices. Feel free to modify these guidelines in a pull request if required. 12 | 13 | #### Table of contents 14 | 15 | - [Getting started with MkDocs](#getting-started-with-mkdocs) 16 | - [What is MkDocs?](#what-is-mkdocs) 17 | - [Great, but what is Markdown?](#great-but-what-is-markdown) 18 | - [Installing MkDocs](#installing-mkdocs) 19 | - [Using MkDocs](#using-mkdocs) 20 | - [Documentation structure](#documentation-structure) 21 | - [Modifying existing pages/sections](#modifying-existing-pages-sections) 22 | - [Adding new pages/sections](#adding-new-pages-sections) 23 | - [Submitting your changes](#submitting-your-changes) 24 | - [General guidelines](#general-guidelines) 25 | 26 | ## Getting started with MkDocs 27 | 28 | ### What is MkDocs? 29 | 30 | [MkDocs](http://www.mkdocs.org/) is a "static site generator" geared towards building project documentation. We chose MkDocs because of its ease of use and, well, if we're honest, so we didn't have to build our own system like we did for the [XenForo 1 Manual](https://xenforo.com/help/manual/). It also makes it insanely easy for us to be able to accept changes from our contributors. 31 | 32 | Not only that, but editing the documentation is as simple as adding or editing files using Markdown. 33 | 34 | ### Great, but what is Markdown? 35 | 36 | Well, generally awesome is what it is 😁 37 | 38 | > Markdown is a text-to-HTML conversion tool for web writers. Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML). 39 | 40 | To put it another way, it's simply a way to write plain text and later have it converted to HTML. All documentation written in MkDocs are simply text files with a `.md` extension. Markdown has become insanely popular over the last few years. If you'd like to learn more about it, GitHub has a [great guide](https://guides.github.com/features/mastering-markdown/) to get you started. 41 | 42 | ### Installing MkDocs 43 | 44 | MkDocs can be installed using a variety of OS package managers, and this is the recommended approach to installing it. 45 | 46 | - [Homebrew](http://brew.sh/) (macOS) 47 | - [Chocolatey](https://chocolatey.org/) (Windows) 48 | - [yum](http://yum.baseurl.org/), [apt-get](https://help.ubuntu.com/community/AptGet/Howto), [DNF](http://dnf.readthedocs.io/en/latest/index.html) (Linux) 49 | 50 | You can also find some more detailed instructions [here](http://www.mkdocs.org/#installation). 51 | 52 | For editing the documentation, installing MkDocs is entirely optional as the documentation can be modified directly via the interface provided on GitHub. If you'd like to learn more about setting up MkDocs you can read the section below. 53 | 54 | ### Using MkDocs 55 | 56 | The first step to using MkDocs alongside this documentation is to pull the documentation down from this repo. You can either use a Git client for this, or use Git on the command line. 57 | 58 | In the desired directory, simply run the following command: 59 | 60 | ``` 61 | git clone git@github.com:xenforo-ltd/docs.git 62 | ``` 63 | 64 | This will create a new directory named `docs` containing the contents of this repo. 65 | 66 | Using the command line, change directory to the new `docs` directory and run the following command: 67 | 68 | ``` 69 | mkdocs serve 70 | ``` 71 | 72 | This will load up a local web server based on the directory contents which is now accessible from the URL `http://localhost:8000/` and it will start watching the documentation for changes and reload automatically. 73 | 74 | ## Documentation structure 75 | 76 | All of the documentation files will appear in the `docs/docs` directory where you will find the top level pages for each section. 77 | 78 | These top level pages are also defined, along with their titles, inside the `docs/mkdocs.yml` file. 79 | 80 | Each of the top level pages are split into sections. Each header section (denoted by a heading starting with `##` characters) will appear in the navigation bar for each page. 81 | 82 | ## Modifying existing pages/sections 83 | 84 | Once you've ascertained the section you would like to change, just edit the file directly in your preferred text editor. You can also edit the pages directly on GitHub. 85 | 86 | ## Adding new pages/sections 87 | 88 | If you'd like to add entirely new pages/sections, you can either add new sections to an exisitng page under an appropriate header (again, denoted by `##` characters) or create new pages entirely. 89 | 90 | Creating new pages involves creating the actual pages themselves, and also modifying the `docs/mkdocs.yml` file to reference those pages. 91 | 92 | We do not generally recommend editing the `docs/mkdocs.yml` file outside of the process of adding new pages. 93 | 94 | ## Submitting your changes 95 | 96 | If you followed the instructions to clone this repo and set up MkDocs, and you want to submit your changes to our repository you will need to create a [pull request](https://git-scm.com/docs/git-request-pull). 97 | 98 | If you are editing/adding the files directly on GitHub, a pull request will be submitted automatically. 99 | 100 | Once your changes have been submitted, they will periodically be reviewed and either approved and merged, rejected, or discussion will take place related to the desired changes before being accepted. 101 | 102 | ## General guidelines 103 | 104 | We do not want to impose too many rules as a barrier to updating our documentation, but please bear the following in mind: 105 | 106 | 1. Changes should generally be limited to editing/adding pages/sections. 107 | 2. Large changes to the overall documentation structure will not be accepted but if they are necessary they should be discussed first by creating an issue. 108 | 3. Similarly, changes to the config `docs/mkdocs.yml` file or changes to the styling of the documentation will not be accepted. 109 | 4. Any content submitted should be written in English and not contain any content that would not circumvent our usual [rules for user generated content](https://xenforo.com/community/help/terms/). 110 | 5. Finally, by submitting changes to the documentation you: 111 | 1. agree that changes you submit can be included in our published documentation 112 | 2. agree that once the changes are approved they can in the future be modified or removed by us or another contributor if that becomes necessary 113 | 3. agree not to contest any subsequent modification or removal of content you have submitted 114 | 4. agree that the documentation content you submit will ultimately be owned by XenForo Ltd. 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2023 XenForo Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XenForo Developer Documentation 2 | 3 | Welcome to the developer documentation for XenForo. 4 | 5 | The documentation is far from finished, but is still a good start to familiarise yourself with: 6 | 7 | - Installing XF from the command line 8 | - The add-on structure in XF 9 | - Some concepts in XF including entities, finders and repositories 10 | - A full tutorial to build your own portal add-on in XF 11 | 12 | Please feel free to post any [issues](https://github.com/xenforo-ltd/docs/issues) you may find with the documentation. If you'd like to contribute changes to our documentation then you should first read our guidelines and other useful information [here](https://github.com/xenforo-ltd/docs/blob/master/CONTRIBUTING.md). 13 | 14 | Please also use the main [XenForo community](https://xenforo.com/community) for help with XF development, providing feedback on XenForo and reporting bugs with the software. 15 | -------------------------------------------------------------------------------- /docs/add-on-structure.md: -------------------------------------------------------------------------------- 1 | # Add-on structure 2 | 3 | In previous versions of XF, there were very few standards and conventions surrounding add-on development. We have done 4 | a lot to change that in XF 2.0. Let's look at some of the changes: 5 | 6 | ## Add-on IDs and add-ons path 7 | 8 | Each installed add-on must have a unique ID, and this ID dictates where on the filesystem that an add-on should store 9 | its files. There are two possible formats for an add-on ID. 10 | 11 | The first "simple" type should be a single word and not contain any special characters. For example, `Demo`. 12 | 13 | Simple add-on IDs must adhere to the following rules: 14 | 15 | - Must only contain a-z or A-Z 16 | - Can contain 0-9 but not at the start of the ID 17 | - Can not contain any special characters such as slashes, dashes or underscores 18 | 19 | The second contains a vendor prefix, so if you release add-ons under a specific brand or company, the add-on ID can 20 | indicate that. For example, `SomeVendor/Demo`. 21 | 22 | The vendor type add-on ID should adhere to the following rules: 23 | 24 | - Must only contain a-z or A-Z 25 | - Can contain a single `/` character but not at the start or the end 26 | - Can contain 0-9 but not at the start of either part of the add-on ID 27 | 28 | Once you have decided what your add-on ID is, we know exactly where the files for this add-on will be stored. All XF 2.0 add-ons are stored within a subdirectory of the `src/addons` directory. 29 | 30 | If you have a simple add-on ID, e.g. `Demo`, the files for your add-on will be stored in the following location: 31 | `src/addons/Demo`. 32 | 33 | If you have a vendor based add-on ID, e.g. `SomeVendor/Demo`, the files will be stored in the following location: 34 | `src/addons/SomeVendor/Demo`. 35 | 36 | The add-on ID you choose will also become your class namespace prefix (see [Namespaces](general-concepts.md#namespaces) for more information). 37 | 38 | ## Recommended version string format 39 | 40 | XF itself uses a MAJOR.MINOR.PATCH principle (e.g. 2.0.0 for the first stable XF2 release) to its version numbering and we recommend a similar approach is taken towards the versioning of your own add-ons. In basic terms, increment the 41 | 42 | - MAJOR version when you make major feature changes, especially changes that break backwards compatibility 43 | - MINOR version when you add functionality preferably in a backwards compatible manner, and 44 | - PATCH version when you make backwards-compatible bug fixes 45 | 46 | ## Recommended version ID format 47 | 48 | Version IDs for add-ons are basic integers which are used for internal version comparisons. It allows us to more easily detect when one version is older than another. Each version of your add-on should increase the version ID by at least 1, but a convention we use internally for XF itself, is potentially useful also for add-ons. Our version IDs are in the format of `aabbccde`. 49 | 50 | - `aa` represents the major version 51 | - `bb` represents the minor version 52 | - `cc` represents the patch version 53 | - `d` represents the state, e.g. `1` for alpha releases, `3` for beta releases, `5` for release candidates and `7` for stable releases 54 | - `e` represents the state version 55 | 56 | For example, an add-on with version string of 1.7.3 release candidate 4 would have an ID of `1070354`. The final stable release XF2 will have an ID of `2000070`. Version 1.5.0 Beta 3 of XF had an ID of `1050033`. Stable version 99.99.99 would have an ID of `99999970`... and maybe you should slow down a bit :wink: 57 | 58 | ## Common add-on files and directories 59 | 60 | There are a number of files and directories within an add-on's directory that have a special purpose and meaning. 61 | 62 | ### addon.json file 63 | 64 | `addon.json` is a file which contains a number of pieces of information which are required to help XF 2.0 identify the 65 | add-on and display information about it in the Admin CP. At minimum, your `addon.json` file should look like this: 66 | 67 | ```json title="addon.json" 68 | { 69 | "title": "My Add-on by Some Company", 70 | "version_string": "2.0.0", 71 | "version_id": 2000070, 72 | "dev": "Some Company" 73 | } 74 | ``` 75 | 76 | A basic file will be created for you automatically when creating the add-on. 77 | 78 | Including a valid `addon.json` file is mandatory for your addon to be recognized but you can always [validate your addon.json file](development-tools.md#validate-your-addonjson-file). 79 | 80 | #### Properties 81 | 82 | | Property | Description | 83 | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 84 | | `legacy_addon_id` | Used to enable automatic handling of addon ID changes when upgrading from XenForo 1 to XenForo 2. | 85 | | `title` | The title of the addon. This will show in the Admin Panel. | 86 | | `description` | A description of the addon. This will show in the Admin Panel. | 87 | | `version_id` | The internal ID used by XenForo to track updates to your addon. This must be incremented every release. | 88 | | `version_string` | The human-readable addon version. This will show in the Admin Panel instead of the `version_id` property. | 89 | | `dev` | The name of the developer of the addon. This will show in the Admin Panel. | 90 | | `dev_url` | If set, the developer's name will show in the Admin Panel as a hyperlink, with this as the target (href). | 91 | | `faq_url` | If set, an FAQ hyperlink will show in the Admin Panel, with this as the target (href). | 92 | | `support_url` | If set, a support hyperlink will show in the Admin Panel, with this as the target (href). | 93 | | `extra_urls` | This allows you to display links to other things related to the add-on (perhaps a bug reports link, a manual - whatever you like). An array of JSON objects, where the key is the link text and the value is the link target (href). | 94 | | `require` | A set of requirements that need to be met for XenForo to allow installation of the addon. See ['The requirements property'](#the-requirements-property) for more information. | 95 | | `icon` | The icon of the resource. This can be a Font Awesome icon name (e.g. `fa-shopping-bag`, or the path to an image file.) | 96 | 97 | ##### The requirements property 98 | 99 | The require property is the standard way of blocking an add-on install or upgrade if the environment doesn't support or meet the requirements. 100 | You can use it to require other add-ons to be installed first, certain PHP extensions to be present or enabled and/or to enforce a minimum PHP version. 101 | 102 | Here's an example snippet: 103 | 104 | ```json title="addon.json" 105 | ... 106 | "require": { 107 | "XF": [2000010, "XenForo 2.0.0+"], 108 | "php": ["5.4.0", "PHP 5.4.0+"], 109 | "php-ext/json": ["*", "JSON extension"] 110 | } 111 | ... 112 | ``` 113 | 114 | Each requirement, is a named array: 115 | 116 | - The name of the array is the product ID (e.g. `XF` or `php`). 117 | - The first array element is the version of the product (e.g. `2000010` or `5.4.0`). You can use use `*` to refer to any version of the product. 118 | - The second element is the human-readable text of that requirement and this is what's used in messages (e.g. `XenForo 2.0.0+` or `PHP 5.4.0+`). 119 | 120 | Here's a summary of the supported product IDs: 121 | 122 | | Product/requirement name | Refers to... | Value | 123 | | -------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 124 | | `XF` | The XenForo installation version. | The XenForo version ID, for example `200010`.
You can get your current XenForo version by checking the top of the `/src/XF.php` file for the `$versionId` definion or by printing the value of `\XF::$versionId`. | 125 | | `php` | The PHP version. | The PHP version, for example `5.4.0`.
It's recommended that you keep this as low as possible; updating a PHP version can be quite a complex task - especially if other add-ons conflict with newer PHP versions. | 126 | | `php-ext/(extension name)` | A PHP extension - where `(extension name)` is the name of the extension. | The PHP extension version.
This is checked using the PHP `version_compare` function, so it even works for version strings in the official full PHP format like `7.1.19-1+ubuntu16.04.1+deb.sury.org+1`. | 127 | | `(any addon ID)` | Any XenForo addon such as `Demo/Addon`.
If you're unsure about an addon's ID, check it's `addon.json` file. | The addon version ID.
You can refer to the [Recommended version ID format](#recommended-version-id-format) for more information. | 128 | 129 | ### hashes.json file 130 | 131 | `hashes.json` is the new way to add support for the File health check system, and the best part is -- it's generated 132 | automatically! 133 | 134 | As part of the build process (more on that later) we will do a quick inventory of all your add-on's files and write the calculated hash of the file contents. 135 | 136 | ### Setup.php file 137 | 138 | `Setup.php` is the new home for any code you require to run during install, upgrade or uninstallation of your add-on. 139 | 140 | We will go into more detail about how to create a Setup class [below](#setup-class). 141 | 142 | ### \_data directory 143 | 144 | The `_data` directory is where the master data for your add-on is stored. Each add-on data type will have its own XML 145 | file (rather than a single one for all types). The hashes for these files are included inside `hashes.json` so we can 146 | ensure that an add-on has complete and consistent data before allowing an add-on to be installed. 147 | 148 | ### \_output directory 149 | 150 | The `_output` directory is not required for a successful installation of an add-on, and shouldn't be included when releasing the add-on. This directory is purely for development purposes and is only used if development mode is enabled (see [Enabling development mode](development-tools.md#enabling-development-mode)). 151 | 152 | Each item of add-on data is stored in a separate file. Mostly they are stored as JSON files, but in the case of phrases they are stored as TXT files and for templates they are stored as HTML/CSS/LESS files. All template types are editable in the filesystem directly, and changes made to these files are written back to the database automatically on load. 153 | 154 | ## Setup class 155 | 156 | To create a Setup class for your add-on, all you need to do is create a file named `Setup.php` in the root of your add-on directory. 157 | 158 | The Setup class should extend `\XF\AddOn\AbstractSetup` which requires, at minimum, to implement `install()`, `upgrade()` and `uninstall()` methods. Here's what a simple add-on Setup class might look like: 159 | 160 | ```php title="Setup.php" 161 | schemaManager()->createTable('xf_demo', function(\XF\Db\Schema\Create $table) 170 | { 171 | $table->addColumn('demo_id', 'int'); 172 | }); 173 | } 174 | 175 | public function upgrade(array $stepParams = []) 176 | { 177 | if ($this->addOn->version_id < 1000170) 178 | { 179 | $this->schemaManager()->alterTable('xf_demo', function(\XF\Db\Schema\Alter $table) 180 | { 181 | $table->addColumn('foo', 'varchar', 10)->setDefault(''); 182 | }); 183 | } 184 | } 185 | 186 | public function uninstall(array $stepParams = []) 187 | { 188 | $this->schemaManager()->dropTable('xf_demo'); 189 | } 190 | } 191 | ``` 192 | 193 | The Setup class also supports running each of the actions in different steps. To implement this behavior your Setup class can use the `StepRunnerInstallTrait`, `StepRunnerUpgradeTrait` and/or `StepRunnerUninstallTrait` [traits](http://php.net/manual/en/language.oop5.traits.php). These implement the required methods automatically, and you just need to add the relevant steps, e.g. `installStep1()`, `upgrade1000170Step1()`, `upgrade1000170Step2()` and `uninstallStep1()`, where `1000170` etc. in the upgrade methods are the add-on version IDs (see [Recommended version ID format](#recommended-version-id-format)). 194 | -------------------------------------------------------------------------------- /docs/controller-basics.md: -------------------------------------------------------------------------------- 1 | # Controller basics 2 | 3 | At a basic level, Controllers are the code that is executed when you visit a page within XF. Controllers are generally responsible for handling user input and passing that user input to the appropriate place which, generally, would be to perform some sort of database action (Model) or load visual content (View). 4 | 5 | When a user clicks a link, the requested URL is routed to a specific controller and controller action. See [Routing basics](routing-basics.md). For example, in XF if you click a URL like `index.php?conversations/add` you will be routed to the `XF\Pub\Controller\Conversation` controller and to the `add` action. 6 | 7 | If you look at this class in the file system (see [Autoloader](general-concepts.md#autoloader) for a description of how classes and file paths map to each other) you will notice that there are a number of methods named with a prefix of `action`. All of these methods indicate a specific controller action. So, to see the code involved when viewing the conversations/add page mentioned above, look in this file for `public function actionAdd()`. 8 | 9 | XF controllers are responsible for returning a reply object which generally consist of one of the following types: 10 | 11 | ## View reply 12 | 13 | This is one of the most common replies you will deal with during XF development. A controller which returns a view reply will usually require up to three arguments to be passed in. A view class (more on that below), a template name, and an array of `$viewParams` which is the data that should be available to the template. 14 | 15 | Here's a typical example of a controller action which returns a View reply: 16 | 17 | ```php 18 | public function actionExample() 19 | { 20 | $hello = 'Hello'; 21 | $world = 'world!'; 22 | 23 | $viewParams = [ 24 | 'hello' => $hello, 25 | 'world' => $world 26 | ]; 27 | return $this->view('Demo:Example', 'demo_example', $viewParams); 28 | } 29 | ``` 30 | 31 | The first argument is the short class name for a specific View class. This class may or may not exist (often it won't need to exist, we'll cover view classes more later) but it should have a roughly unique name for the controller and action. As with other [Short class names](general-concepts.md#short-class-names), the particular short class name above will resolve to `Demo\Pub\View\Example`. Again, `Pub` is inferred automatically from the controller type. 32 | 33 | The second argument is the template name. In this case, we're looking for a template named `demo_example`. 34 | 35 | The third argument is an array of template parameters/variables that should be available to the view. This array should generally be `key => value` pairs. The above example is passing two template params to the template. The `key` part of the array indicates the name of the variable available within the template. The `value` part of the array indicates the value. 36 | 37 | So, if we had the following contents in the `demo_example` template: 38 | 39 | ```html 40 | {$hello} {$world} 41 | ``` 42 | 43 | The template would output the following: 44 | 45 | ```html 46 | Hello world! 47 | ``` 48 | 49 | ## Redirect reply 50 | 51 | This reply is returned when you wish to redirect a user to a different URL after they have completed some sort of action. 52 | 53 | A common use case here is after a user has submitted data through a form you may wish to redirect them to a different page, for example returning a user to a list of items. 54 | 55 | Here's an example of a typical controller action that performs a redirect: 56 | 57 | ```php 58 | public function actionRedirect() 59 | { 60 | return $this->redirect($this->buildLink('demo/example'), 'This is a redirect message.', 'permanent'); 61 | } 62 | ``` 63 | 64 | The first argument is the URL to redirect to. This example will redirect the user to the `index.php?demo/example` URL. 65 | 66 | The second argument will only display if the form is submitted over an AJAX request which opts to prevent redirecting. The result will be a "flash message" which appears from the top of the screen with your chosen message. You do not have to supply your own message. If it is not provided it will default to "Your changes have been saved". 67 | 68 | The third argument defaults to `temporary`, but you can also opt to set this to permanent as per the example. The only difference here is the type of HTTP response code provided by the server. Temporary is ideal in most cases, and this will respond with a 303 code. `permanent` will issue a 301 response code. 69 | 70 | Although you can trigger a permanent redirect in this way, there's actually a specific method for this, which can be used as follows. It also takes a 'message' argument, but as above it is optional. 71 | 72 | ```php 73 | public function actionRedirect() 74 | { 75 | return $this->redirectPermanently($this->buildLink('demo/example')); 76 | } 77 | ``` 78 | 79 | ## Error reply 80 | 81 | As the name suggests, this reply is what you will return if you need to display an error to the user. It's somewhat simple, here's an example: 82 | 83 | ```php 84 | public function actionError() 85 | { 86 | return $this->error('Unfortunately the thing you are looking for could not be found.', 404); 87 | } 88 | ``` 89 | 90 | There are only two arguments supported here. The first is the error message you want to display, and the second is the HTTP response code you want the server to send. 404 would represent an appropriate response when something was not found. 91 | 92 | ## Message reply 93 | 94 | This reply is very much similar to the error reply, and supports the same arguments. The main difference is, in terms of appearance, the message displayed is not presented as an error. 95 | 96 | ## Exception reply 97 | 98 | It is sometimes necessary to interrupt the normal flow of your controller code, and reply with an Exception instead. Exception replies do not necessarily have to represent an error; for example, they can be used to force your controller to perform a redirect. However, typically, they will often be used to halt the flow of your controller to display an error, as in the following example: 99 | 100 | ```php 101 | public function actionException() 102 | { 103 | throw $this->exception($this->error('An unexpected error occurred')); 104 | } 105 | ``` 106 | 107 | Exception replies only accept a single argument, and actually that argument must be some other form of Reply object, such as an [Error reply](#error-reply). This particular example throws an exception, and the entire controller code at that point will stop, and a standard error will be displayed. 108 | 109 | Note that exception replies must be "thrown" using `throw` rather than being "returned" with `return`. 110 | 111 | ## Reroute reply 112 | 113 | Under certain conditions, it is necessary to reroute a user to an entirely different controller or action within the same controller, without performing a full redirect, without changing the URL the user has landed on, and without having to duplicate the code of the target action. 114 | 115 | That looks a little bit like this: 116 | 117 | ```php 118 | public function actionReroute() 119 | { 120 | return $this->rerouteController(__CLASS__, 'error'); 121 | } 122 | 123 | public function actionError() 124 | { 125 | return $this->error('Oops! Something went wrong!'); 126 | } 127 | ``` 128 | 129 | In this particular example, if a user navigated to the `index.php?demo/reroute` URL, they would see the error reply from the `actionError()` method. They would not be redirected, nor would the URL in their browser change; they would simply just receive the reply from the error action. 130 | 131 | The reroute reply also supports a third argument which allows various parameters to be passed from one controller action to the other. This can either be an array or a `ParameterBag` object (more on that later). 132 | 133 | ## Modifying a controller action reply (properly) 134 | 135 | In the [Extending classes](general-concepts.md#extending-classes) section, we've already seen how simple it is to extend a class, but extra care needs to be taken when extending a controller action that already exists. 136 | 137 | Unless you have a specific need to override an existing action entirely, and replace it with something new (which is generally not recommended), instead you should be modifying the existing reply of the parent class. That is done quite simply, as an example let's modify the view reply from the [View reply](#view-reply) example above. 138 | 139 | ```php 140 | public function actionExample() 141 | { 142 | $reply = parent::actionExample(); 143 | 144 | return $reply; 145 | } 146 | ``` 147 | 148 | Assuming the above is added to an extended controller where the `actionExample()` method already exists, the above doesn't actually do anything other than return the original view reply. Let's now change the existing `hello` parameter to read "Bonjour" instead of "Hello". 149 | 150 | ```php 151 | public function actionExample() 152 | { 153 | $reply = parent::actionExample(); 154 | 155 | if ($reply instanceof \XF\Mvc\Reply\View) 156 | { 157 | $reply->setParam('hello', 'Bonjour'); 158 | } 159 | 160 | return $reply; 161 | } 162 | ``` 163 | 164 | Because a controller reply can actually represent a number of different objects that have different behaviors and methods, it is imperative that we only attempt to extend the correct reply type. We do that in the example above by checking to see if the parent `$reply` object is actually a `View` type. If we didn't do this, we extended this action and the controller action replies with a redirect instead, then there would likely be an error. 165 | 166 | Before extending this action visiting this page would display "Hello world!". After extending it, the view will now display "Bonjour world!". 167 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="default"] { 2 | --md-primary-fg-color: hsl(205 80% 30%); 3 | --md-primary-fg-color--light: hsl(205 80% 40%); 4 | --md-primary-fg-color--dark: hsl(205 80% 30%); 5 | --md-primary-bg-color: hsl(204 80% 80%); 6 | --md-primary-bg-color--light: hsl(204 80% 95%); 7 | 8 | --md-accent-fg-color: hsl(205 80% 40%); 9 | --md-accent-fg-color--transparent: hsl(205 80% 40% / 0.1); 10 | --md-accent-bg-color: hsl(205 80% 80%); 11 | --md-accent-bg-color--light: hsl(205 80% 95%); 12 | 13 | --md-default-fg-color: hsl(0 0% 8%); 14 | --md-default-fg-color-light: hsl(0 0% 8% / 0.7); 15 | --md-default-fg-color-lighter: hsl(0 0% 8% / 0.3); 16 | --md-default-fg-color-lightest: hsl(0 0% 8% / 0.12); 17 | --md-default-bg-color: hsl(0 0% 100%); 18 | --md-default-bg-color--light: hsl(0 0% 100% / 0.7); 19 | --md-default-bg-color--lighter: hsl(0 0% 100% / 0.3); 20 | --md-default-bg-color--lightest: hsl(0 0% 100% / .12); 21 | 22 | --md-typeset-a-color: hsl(205 80% 40%); 23 | 24 | --md-footer-fg-color: hsl(206 80% 80%); 25 | --md-footer-fg-color--light: hsl(206 80% 95%); 26 | --md-footer-fg-color--lighter: hsl(206 80% 95% / 0.5); 27 | --md-footer-bg-color: hsl(205 80% 22%); 28 | --md-footer-bg-color--dark: hsl(205 80% 22% / 0.5); 29 | } 30 | 31 | [data-md-color-scheme="slate"] { 32 | --md-primary-fg-color: hsl(205 60% 10%); 33 | --md-primary-fg-color--light: hsl(205 60% 20%); 34 | --md-primary-fg-color--dark: hsl(205 60% 10%); 35 | --md-primary-bg-color: hsl(204 60% 60%); 36 | --md-primary-bg-color--light: hsl(204 60% 75%); 37 | 38 | --md-accent-fg-color: hsl(205 60% 60%); 39 | --md-accent-fg-color--transparent: hsl(205 60% 60% / 0.1); 40 | --md-accent-bg-color: hsl(205 60% 20%); 41 | --md-accent-bg-color--light: hsl(205 60% 40%); 42 | 43 | --md-default-fg-color: hsl(0 0% 88%); 44 | --md-default-fg-color-light: hsl(0 0% 88% / 0.7); 45 | --md-default-fg-color-lighter: hsl(0 0% 88% / 0.3); 46 | --md-default-fg-color-lightest: hsl(0 0% 88% / 0.12); 47 | --md-default-bg-color: hsl(0 0% 8%); 48 | --md-default-bg-color--light: hsl(0 0% 8% / 0.7); 49 | --md-default-bg-color--lighter: hsl(0 0% 8% / 0.3); 50 | --md-default-bg-color--lightest: hsl(0 0% 8% / .12); 51 | 52 | --md-typeset-a-color: hsl(205 60% 60%); 53 | 54 | --md-footer-fg-color: hsl(206 60% 60%); 55 | --md-footer-fg-color--light: hsl(206 60% 75%); 56 | --md-footer-fg-color--lighter: hsl(206 60% 75% / 0.5); 57 | --md-footer-bg-color: hsl(205 60% 2%); 58 | --md-footer-bg-color--dark: hsl(205 60% 2% / 0.5); 59 | } 60 | 61 | .md-logo { 62 | width: 11.5rem; 63 | } 64 | 65 | .md-logo:hover { 66 | opacity: 1; 67 | } 68 | 69 | .md-header__button.md-logo img, 70 | .md-nav__title .md-nav__button.md-logo img { 71 | width: 100px; 72 | height: 26px; 73 | } 74 | 75 | th:first-of-type, 76 | td:first-of-type { 77 | white-space: nowrap; 78 | } 79 | -------------------------------------------------------------------------------- /docs/designing-styles.md: -------------------------------------------------------------------------------- 1 | # Designing styles 2 | 3 | In XF2 we have introduced an all new way to build and edit styles called "Designer mode". Designer mode is a collection of CLI tools that allow you to modify certain templates within a style directly on the file system. It also outputs various metadata and information about style properties which is useful for version control and collaboration. 4 | 5 | ## Enabling designer mode 6 | 7 | The first step to enabling designer mode is to enable it in `config.php`: 8 | 9 | ```php title="src/config.php" 10 | $config['designer']['enabled'] = true; 11 | ``` 12 | 13 | Optionally, you can also specify a different path for designer mode files to exist on the file system. The following represents the default location. To change the location, add the below to your `config.php` and modify the path accordingly: 14 | 15 | ```php title="src/config.php" 16 | $config['designer']['basePath'] = 'src/styles'; 17 | ``` 18 | 19 | ## Enabling designer mode for a style 20 | 21 | Designer mode must be explicitly enabled for each style. We enable designer mode on a style by using the CLI and specifying the style ID of the style, and choosing a "designer mode ID": 22 | 23 | ```sh title="Terminal" 24 | php cmd.php xf-designer:enable [style_id] [designer_mode_id] 25 | ``` 26 | 27 | The designer mode ID is the identifier you will use for future commands related to designer mode. Once enabled, the current modified components of the style will be exported to the `[basePath]/[designer_mode_id]` directory. 28 | 29 | When enabling designer mode for this style if that directory already exists you will be given a choice to make as to whether we should overwrite the current contents of that directory from the style, or whether we should overwrite the current style from the current contents of that directory. 30 | 31 | ## Disabling designer mode for a style 32 | 33 | To disable designer mode for a style, you just run the following CLI command: 34 | 35 | ```sh title="Terminal" 36 | php cmd.php xf-designer:disable [designer_mode_id] 37 | ``` 38 | 39 | By default, this will keep the copy of the designer mode output on the file system. To remove the data, you can run the same command with the `--clear` option: 40 | 41 | ```sh title="Terminal" 42 | php cmd.php xf-designer:disable [designer_mode_id] --clear 43 | ``` 44 | 45 | ## What is output and where? 46 | 47 | It is important to remember that a style within XF only consists of what is **modified in that style**. This means that designer mode output will only consist of what has been modified in the style. Templates and style properties which are modified in a parent style is not output. 48 | 49 | ### Templates 50 | 51 | Templates will be output to the `[basePath]/[designer_mode_id]/templates` directory. Within that directory you may have another directory for each type (e.g. admin, email and public). 52 | 53 | The templates will be output in HTML format and are directly editable on the file system. Changes made on the file system are imported and compiled when that template is loaded on a page. Similarly, you can revert a template by deleting it from the file system (if it was previously modified). 54 | 55 | ### Style properties and groups 56 | 57 | Style properties and groups will be output to the `[basePath]/[designer_mode_id]/style_properties` and `[basePath]/[designer_mode_id]/style_property_groups` directories. They are exported in JSON format and serve as a useful way to monitor changes to these files via a version control system. 58 | 59 | It is not recommended to modify these files directly as changes to them will **not** be automatically imported like they are with templates. 60 | 61 | ## Modifying a specific template 62 | 63 | Bearing in mind that a style represents components which are modified within that style only, when designer mode is enabled, the file system will also contain only components which are modified within that style only. It would not be possible to output the effective version of each template and style property. 64 | 65 | To mark a template as modified within a style, you can do it in the usual way by editing it in the Admin CP. Templates and style properties modified in the Admin CP will automatically be written out to the file system if designer mode is enabled. However, it would likely be more convenient to modify or "touch" a template using a CLI command: 66 | 67 | ```sh title="Terminal" 68 | php cmd.php xf-designer:touch-template [designer_mode_id] [template_type:template_title] 69 | ``` 70 | 71 | As long as the specified template exists in a parent or the master style, it will be copied to the current style and output to the file system. You can then modify the template directly in the file system. 72 | 73 | If you would like to create an entirely custom template in your style (that doesn't exist in any other style within the tree), you can use the same command but you would just pass the `--custom` option: 74 | 75 | ```sh title="Terminal" 76 | php cmd.php xf-designer:touch-template [designer_mode_id] [template_type:template_title] --custom 77 | ``` 78 | 79 | ## Other useful commands 80 | 81 | There are a number of other useful commands relating to designer mode: 82 | 83 | ### Export from database 84 | 85 | This command is usually automatically run when designer mode is enabled on a style, but if for some reason you would like to overwrite the file system copy with what is currently in the database, then you can run the following command: 86 | 87 | ```sh title="Terminal" 88 | php cmd.php xf-designer:export [designer_mode_id] 89 | ``` 90 | 91 | It's also possible to export only specific types, e.g. `xf-designer:export-templates`. 92 | 93 | ### Import from file system 94 | 95 | This command will overwrite the database copy of the style with what is on the file system: 96 | 97 | ```sh title="Terminal" 98 | php cmd.php xf-designer:import [designer_mode_id] 99 | ``` 100 | 101 | It's also possible to import only specific types, e.g. `xf-designer:import-templates`. 102 | 103 | ### Sync templates 104 | 105 | This command is similar to importing templates (see above) but instead of overwriting everything it will only import templates and recompile them if the metadata has changed. It will also apply version number updates accordingly. 106 | 107 | ```sh title="Terminal" 108 | php cmd.php xf-designer:sync-templates [designer_mode_id] 109 | ``` 110 | 111 | ### Revert template 112 | 113 | This command can be used to revert a template, effectively deleting the custom version from the current style. 114 | 115 | ```sh title="Terminal" 116 | php cmd.php xf-designer:revert-template [designer_mode_id] [template_type:template_title] 117 | ``` 118 | 119 | It's also possible to trigger a revert by removing the template from the file system. 120 | -------------------------------------------------------------------------------- /docs/development-tools.md: -------------------------------------------------------------------------------- 1 | # Development tools 2 | 3 | XF2 provides developers with a number of built in tools you can use to expedite development of add-ons and we'll go through some of these below. 4 | 5 | ## Debug mode 6 | 7 | Debug mode can be enabled in your `config.php` which will allow you to access certain development tools in the Admin CP (such as creating routes, permissions, admin navigation etd.) and it will also enable an output at the bottom of every page which details how long the page took to process, how many queries were executed to render the page and how much memory was used. A tooltip containing information about the current controller, action and template name is available on hover. You can also click on the time output and this will give you a detailed look at exactly what queries ran and the stack trace that led to that query being executed. 8 | 9 | You can enable debug mode by adding the following to `config.php`: 10 | 11 | ```php title="src/config.php" 12 | $config['debug'] = true; 13 | ``` 14 | 15 | ## Enabling development mode 16 | 17 | Development mode is a special mode, activated in your `config.php` file which will trigger XF to automatically write out your development files to your `_output` directory. This mode needs to be enabled for filesystem template editing to be enabled. As development mode will be writing files to your file system it is important to ensure you have the appropriate file permissions in place. This may vary depending on environment, but a typical configuration would be to ensure that for any add-on you are working on, you have its `_output` directory set chmod to `0777`. For example, if you are working on an add-on with an ID of `Demo`, its development output will be written out to `src/addons/Demo/_output` and therefore that directory will need to be fully writable. 18 | 19 | Enabling development mode, also enables [debug mode](#debug-mode) automatically. 20 | 21 | To enable development mode, add the following lines to your `config.php` file: 22 | 23 | ```php title="src/config.php" 24 | $config['development']['enabled'] = true; 25 | $config['development']['defaultAddOn'] = 'SomeCompany/MyAddOn'; 26 | ``` 27 | 28 | The `defaultAddOn` value is optional, but adding that will automatically populate the specified add-on in the XF Admin CP when creating new content which will be associated to an add-on. 29 | 30 | In addition to the above, you may find it necessary to add some additional configuration, especially if you are using more than one XF installation. 31 | 32 | ```php title="src/config.php" 33 | $config['enableMail'] = false; 34 | ``` 35 | 36 | This will disable all mail from being sent from your board. This is especially important if you are using a copy of live data with real users and real email addresses (though we would advise against this!). 37 | 38 | As an alternative to disabling mail directly, you may want to consider using a service such as [MailTrap.io](https://mailtrap.io). This provides you with a free mailbox that will receive all emails sent from your board, which is very useful for testing any emails your new add-on may be sending. 39 | 40 | ```php title="src/config.php" 41 | $config['cookie']['prefix'] = 'anything_'; 42 | ``` 43 | 44 | If you're using two or more XF installs on the same domain, you may experience issues with cookies being overwritten, which is caused by the installations sharing the same cookie prefix. It's therefore recommended to ensure you change the cookie prefix for each XF install you have set up. Without doing that, you will experience issues, for example, getting logged out of one XF install when logging into another. 45 | 46 | ## Preventing frequent logout for users with a dynamic IP 47 | 48 | XenForo sessions are usually bound to your IP address. If your IP address is dynamic or you are using a VPN, proxy or load balancer, you may get loggout frequently. You can prevent this by adding the following to your `src/config.php` file: 49 | 50 | ```php title="src/config.php" 51 | $c->extend('session', function(\XF\Session\Session $session) 52 | { 53 | $session->setConfig([ 54 | 'ipv4CidrMatch' => 0, 55 | 'ipv6CidrMatch' => 0 56 | ]); 57 | return $session; 58 | }); 59 | ``` 60 | 61 | !!! warning 62 | You should never apply the above code to the config of a live/production site. 63 | 64 | ## Development commands 65 | 66 | XF 2.0 ships with a number of general development and add-on CLI commands which are aimed to help you develop more efficiently or even possibly automate/script some common processes. 67 | 68 | In this section we'll go through some of the common tools and explain what they do. 69 | 70 | ## Add-on specific commands 71 | 72 | ### Creating a new add-on 73 | 74 | ```sh title="Terminal" 75 | php cmd.php xf-addon:create 76 | ``` 77 | 78 | The `xf-addon:create` command is how to initially set up and create a new add-on. Once it runs, all you need to answer are some basic questions: 79 | 80 | - Enter an ID for this add-on 81 | - Enter a title 82 | - Enter a version ID (e.g. 1000010) 83 | - Enter a version string (e.g. 1.0.0 Alpha) 84 | 85 | You will then be given the option to create the add-on and write out its addon.json file, and asked some questions about whether you want to add a Setup.php file. 86 | 87 | ### Export \_data .XML files 88 | 89 | ```sh title="Terminal" 90 | php cmd.php xf-addon:export [addon_id] 91 | ``` 92 | 93 | This command is what you will use to export all of your add-on's data to XML files inside the `_data` directory. It exports the data from what is currently in the database (rather than from the development output files). 94 | 95 | ### Bump your add-on version 96 | 97 | ```sh title="Terminal" 98 | php cmd.php xf-addon:bump-version [addon_id] --version-id 1020370 --version-string 1.2.3 99 | ``` 100 | 101 | !!! note 102 | If your version string contains spaces, you'll need to surround it with quotes. 103 | 104 | This command takes the add-on ID for your add-on, the new version ID and the new version string. This enables you to bump the version of your add-on in a single step, without having to perform upgrades and rebuilds yourself. The options above are optional, and if they are not provided you will be prompted for them. If you only specify the version ID, we will try and infer the correct version string from that automatically if it matches our [Recommended version ID format](add-on-structure.md#recommended-version-id-format). Once the command completes, it updates the `addon.json` file automatically and the database with the correct version details. 105 | 106 | ### Sync your addon.json to the database 107 | 108 | ```sh title="Terminal" 109 | php cmd.php xf-addon:sync-json [addon_id] 110 | ``` 111 | 112 | Sometimes you might prefer to edit the JSON file directly with certain details. This could be the version, or a new icon, or a change of title or description. Changing the JSON in this way can cause the add-on system to believe there are pending changes or that the add-on is upgradeable. A rebuild or upgrade can be a destructive operation if you haven't yet exported your current data. Therefore, running this command is recommended as a way of importing that data in without affecting your existing data. 113 | 114 | ### Validate your addon.json file 115 | 116 | ```sh title="Terminal" 117 | php cmd.php xf-addon:validate-json [addon_id] 118 | ``` 119 | 120 | If you'd like to check your JSON file contains the correct content and in the correct format, you can now validate it. The validator will check that the content can be decoded, that it contains all of the correct required fields (such as title and version ID) and also checks for the presence of the optional keys (such as description and icon). If any keys are missing, you will be offered to have the issues fixed for you. We also check to see if there are any unexpected fields within the JSON file. These may be deliberate or represent typos. You can run the command manually or the command will be run automatically while building your release. 121 | 122 | ### Run a specific Setup step 123 | 124 | Sometimes it's useful to check that your Setup class steps function correctly, without having to go through the process of uninstalling and reinstalling. 125 | 126 | There are three commands which help with this. These commands will only work with Setup classes that are built using the default `StepRunner` traits. 127 | 128 | #### Run an install step 129 | 130 | ```sh title="Terminal" 131 | php cmd.php xf-addon:install-step [addon_id] [step] 132 | ``` 133 | 134 | #### Run an upgrade step 135 | 136 | ```sh title="Terminal" 137 | php cmd.php xf-addon:upgrade-step [addon_id] [version] [step] 138 | ``` 139 | 140 | #### Run an uninstall step 141 | 142 | ```sh title="Terminal" 143 | php cmd.php xf-addon:uninstall-step [addon_id] [step] 144 | ``` 145 | 146 | ## Building an add-on release 147 | 148 | Once all of the hard work has been done, it's a shame to have to go through a number of other processes before you can actually release it. Even the process of collecting all of the files into the correct place and creating the ZIP file manually can be time consuming and prone to errors. We can take care of that automatically, including generating the `hashes.json` file, with one simple command. 149 | 150 | ```sh title="Terminal" 151 | php cmd.php xf-addon:build-release [addon_id] 152 | ``` 153 | 154 | When you run this command, it will first run the `xf-addon:export` command before then collecting all of your files together into a temporary `_build` directory and writing them to a ZIP file. The finished ZIP will also include the `hashes.json` file. Once the ZIP has been created it will be saved to your `_releases` directory named and named `-.zip`. 155 | 156 | ### Customizing the build process 157 | 158 | Aside from just creating the release ZIP there may be additional files you wish to include in your ZIP, other more advanced build processes you want to run such as minifying or concatenating JS or running certain shell commands. All of this can be taken care of in your `build.json` file. This is a typical `build.json` file: 159 | 160 | ```json title="build.json" 161 | { 162 | "additional_files": [ 163 | "js/demo/portal" 164 | ], 165 | "minify": [ 166 | "js/demo/portal/a.js", 167 | "js/demo/portal/b.js" 168 | ], 169 | "rollup": { 170 | "js/demo/portal/ab-rollup.js": [ 171 | "js/demo/portal/a.min.js", 172 | "js/demo/portal/b.min.js" 173 | ] 174 | }, 175 | "exec": [ 176 | "echo '{title} version {version_string} ({version_id}) has been built successfully!' > 'src/addons/Demo/Portal/_build/built.txt'" 177 | ] 178 | } 179 | ``` 180 | 181 | If you have assets, such as JavaScript, which need to be served outside of your add-on directory, you can tell the build process to copy files or directories using the `additional_files` array within `build.json`. During development it isn't always feasible to keep files outside of your add-on directory, so if you prefer, you can keep the files in your add-on `_files` directory instead. When copying the additional files, we will check there first. 182 | 183 | If you ship some JS files with your add-on, you may want to minify those files for performance reasons. You can specify which files you want to minify right inside your `build.json`. You can list these as an array or you can just specify it as `'*'` which will just minify everything in your `js` directory as long as that path has JS files within it after copying the additional files to the build. Any files minified will automatically have a suffix of `.min.js` instead of `.js` and the original files will still be in the package. 184 | 185 | You may prefer to roll up your multiple JS files into a single file. If you do, you can use the `rollup` array to define that. The key is the resulting combined filename, and the items within that array are the paths to the JS files that will be combined into a single file. 186 | 187 | Finally, you may have certain processes that need to be run just before the package is built and finalised. This could be any combination of things. Ultimately, if it is a command that can be run from the shell (including PHP scripts) then you can specify it here. The example above is of course fairly useless, but it does at least demonstrate that certain placeholders can be used. These placeholders are replaced with scalar values you can get from the `XF\AddOn\AddOn` object which is generally any value available in the `addon.json` file, or the `AddOn` entity. 188 | 189 | ## Development commands 190 | 191 | There are actually quite a few development related commands, but only the two most important ones are being covered here. 192 | 193 | To use any of these commands, you must have [development mode](#enabling-development-mode) enabled in your 194 | `config.php` file. 195 | 196 | !!! warning 197 | Both of the following commands can potentially cause data loss if there is a situation whereby the database and `_output` 198 | directory become out of sync. It is always recommended to use a VCS (Version Control System) such as 199 | [GitHub](https://github.com) to mitigate the impact of such mistakes. 200 | 201 | ### Import development output 202 | 203 | ```sh title="Terminal" 204 | php cmd.php xf-dev:import --addon [addon_id] 205 | ``` 206 | 207 | Running this command will import all of the development output files from your add-on `_output` directory into the 208 | database. 209 | 210 | ### Export development output 211 | 212 | ```sh title="Terminal" 213 | php cmd.php xf-dev:export --addon [addon_id] 214 | ``` 215 | 216 | This will export all data currently associated to your add-on in the database to files within your 217 | `_output` directory. 218 | 219 | ## Debugging code 220 | 221 | It should be possible to set up your favourite debugger tool (XDebug, Zend Debugger etc.) to work with XF2. Though, sometimes, debugging code can be as rudimentary as just quickly seeing what value (or value type) a variable holds at a given time. 222 | 223 | ### Dump a variable 224 | 225 | PHP of course has a tool built-in to handle this. You'll likely know it as `var_dump()`. XF ships with two replacements for this: 226 | 227 | ```php 228 | \XF::dump($var); 229 | \XF::dumpSimple($var); 230 | ``` 231 | 232 | The simple version mostly just dumps out the value of a variable in plain text. For example, if you just use it to dump the value of an array, you will see an output at the top of the page like this: 233 | 234 | ```title="Dump" 235 | array(2) { 236 | ["user_id"] => int(1) 237 | ["username"] => string(5) "Admin" 238 | } 239 | ``` 240 | 241 | This is actually the same output as a standard var_dump, but slightly modified for readability and wrapped inside `
` tags to ensure whitespace is maintained when rendering.
242 | 
243 | The alternative is actually a component named VarDumper from the Symfony project. It outputs HTML, CSS and JS to create a much more functional and potentially easier to read output. It allows you to collapse certain sections, and for certain values which can output a considerable amount of data, such as objects, it can collapse those sections automatically.
244 | 


--------------------------------------------------------------------------------
/docs/entities-finders-repositories.md:
--------------------------------------------------------------------------------
  1 | # Entities, finders and repositories
  2 | 
  3 | There are a number of ways to interact with data within XF2. In XF1 this was mostly geared towards writing out raw SQL statements inside Model files. The approach in XF2 has moved away from this, and we have added a number of new ways in its place. We'll first look at the preferred method for performing database queries - the finder.
  4 | 
  5 | ## The Finder
  6 | 
  7 | We have introduced a new "Finder" system which allows queries to be built up programmatically in a object oriented way so that raw database queries do not need to be written. The Finder system works hand in hand with the Entity system, which we talk about in more detail below. The first argument passed into the finder method is the short class name for the Entity you want to work with. Let's just convert some of the queries mentioned in the section above to use the Finder system instead. For example, to access a single user record:
  8 | 
  9 | ```php
 10 | $finder = \XF::finder('XF:User');
 11 | $user = $finder->where('user_id', 1)->fetchOne();
 12 | ```
 13 | 
 14 | One of the main differences between the direct query approach and using the Finder is that the base unit of data returned by the Finder is not an array. In the case of a Finder object which calls the `fetchOne` method (which only returns a single row from the database), a single Entity object will be returned.
 15 | 
 16 | Let's look at a slightly different approach which will return multiple rows:
 17 | 
 18 | ```php
 19 | $finder = \XF::finder('XF:User');
 20 | $users = $finder->limit(10)->fetch();
 21 | ```
 22 | 
 23 | This example will query 10 records from the xf_user table, and it will return them as an `ArrayCollection` object. This is a special object which acts similarly to an array, in that it is traversable (you can loop through it) and it has some special methods that can tell you the total number of entries it has, grouping by certain values, or other array like operations such as filtering, merging, getting the first or last entry etc.
 24 | 
 25 | Finder queries generally should be expected to retrieve all columns from a table, so there's no specific equivalent to fetch only certain values certain columns.
 26 | 
 27 | Instead, to get a single value, you would just fetch one entity and read the value directly from that:
 28 | 
 29 | ```php
 30 | $finder = \XF::finder('XF:User');
 31 | $username = $finder->where('user_id', 1)->fetchOne()->username;
 32 | ```
 33 | 
 34 | Similarly, to get an array of values from a single column, you can use the `pluckFrom` method:
 35 | 
 36 | ```php
 37 | $finder = \XF::finder('XF:User');
 38 | $usernames = $finder->limit(10)->pluckFrom('username')->fetch();
 39 | ```
 40 | 
 41 | So far we've seen the Finder apply somewhat simple where and limit constraints. So let's look at the Finder in more detail, including a bit more detail about the `where` method itself.
 42 | 
 43 | ### where method
 44 | 
 45 | The `where` method can support up to three arguments. The first being the condition itself, e.g. the column you are querying. The second would ordinarily be the operator. The third is the value being searched for. If you supply only two arguments, as you have seen above, then it automatically implies the operator is `=`. Below is a list of the other operators which are valid:
 46 | 
 47 | - `=`
 48 | - `<>`
 49 | - `!=`
 50 | - `>`
 51 | - `>=`
 52 | - `<`
 53 | - `<=`
 54 | - `LIKE`
 55 | - `BETWEEN`
 56 | 
 57 | So, we could get a list of the valid users who registered in the last 7 days:
 58 | 
 59 | ```php
 60 | $finder = \XF::finder('XF:User');
 61 | $users = $finder->where('user_state', 'valid')->where('register_date', '>=', time() - 86400 * 7)->fetch();
 62 | ```
 63 | 
 64 | As you can see you can call the `where` method as many times as you like, but in addition to that, you can choose to pass in an array as the only argument of the method, and build up your conditions in a single call. The array method supports two types, both of which we can use on the query we built above:
 65 | 
 66 | ```php
 67 | $finder = \XF::finder('XF:User');
 68 | $users = $finder->where([
 69 |     'user_state' => 'valid',
 70 |     ['register_date', '>=', time() - 86400 * 7]
 71 | ])
 72 | ->fetch();
 73 | ```
 74 | 
 75 | It wouldn't usually be recommended or clear to mix the usage like this, but it does demonstrate the flexibility of the method somewhat. Now that the conditions are in an array, we can either specify the column name (as the array key) and value for an implied `=` operator or we can actually define another array containing the column, operator and value.
 76 | 
 77 | ### whereOr method
 78 | 
 79 | With the above examples, both conditions need to be met, i.e. each condition is joined by the `AND` operator. However, sometimes it is necessary to only meet part of your condition, and this is possible by using the `whereOr` method. For example, if you wanted to search for users who are either not valid or have posted zero messages, you can build that as follows:
 80 | 
 81 | ```php
 82 | $finder = \XF::finder('XF:User');
 83 | $users = $finder->whereOr(
 84 |     ['user_state', '<>', 'valid'],
 85 |     ['message_count', 0]
 86 | )->fetch();
 87 | ```
 88 | 
 89 | Similar to the example in the previous section, as well as passing up to two conditions as separate arguments, you can also just pass an array of conditions to the first argument:
 90 | 
 91 | ```php
 92 | $finder = \XF::finder('XF:User');
 93 | $users = $finder->whereOr([
 94 |     ['user_state', '<>', 'valid'],
 95 |     ['message_count', 0],
 96 |     ['is_banned', 1]
 97 | ])->fetch();
 98 | ```
 99 | 
100 | ### with method
101 | 
102 | The `with` method is essentially equivalent to using the `INNER|LEFT JOIN` syntax, though it relies upon the Entity having had its "Relations" defined. We won't go into that until the next page, but this should just give you an understanding of how it works. Let's now use the Thread finder to retrieve a specific thread:
103 | 
104 | ```php
105 | $finder = \XF::finder('XF:Thread');
106 | $thread = $finder->with('Forum', true)->where('thread_id', 123)->fetchOne();
107 | ```
108 | 
109 | This query will fetch the Thread entity where the `thread_id = 123` but it will also do a join with the xf_forum table, behind the scenes. In terms of controlling how to do an `INNER JOIN` rather than a `LEFT JOIN`, that is what the second argument is for. In this case we've set the "must exist" argument to true, so it will flip the join syntax to using `INNER` rather than the default `LEFT`.
110 | 
111 | We'll go into more detail about how to access the data fetched from this join in the next section.
112 | 
113 | It's also possible to pass an array of relations into the `with` method to do multiple joins.
114 | 
115 | ```php
116 | $finder = \XF::finder('XF:Thread');
117 | $thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();
118 | ```
119 | 
120 | This would join to the xf_user table to get the thread author too. However, with the second argument there still being `true`, we might not need to do an `INNER` join for the user join, so, we could just chain the methods instead:
121 | 
122 | ```php
123 | $finder = \XF::finder('XF:Thread');
124 | $thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();
125 | ```
126 | 
127 | ### order, limit and limitByPage methods
128 | 
129 | #### order method
130 | 
131 | This method allows you to modify your query so the results are fetched in a specific order. It takes two arguments, the first is the column name, and the second is, optionally, the direction of the sort. So, if you wanted to list the 10 users who have the most messages, you could build the query like this:
132 | 
133 | ```php
134 | $finder = \XF::finder('XF:User');
135 | $users = $finder->order('message_count', 'DESC')->limit(10);
136 | ```
137 | 
138 | !!! note
139 |     Now is probably a good time to mention that finder methods can mostly be called in any order. For example: `$threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch();`
140 |     Although if you wrote a MySQL query in that order you'd certainly encounter some syntax issues, the Finder system will still build it all in the correct order and the above code, although odd looking and probably not recommended, is perfectly valid.
141 | 
142 | As with a standard MySQL query, it is possible to order a result set on multiple columns. To do that, you can just call the order method again. It's also possible to pass multiple order clauses into the order method using an array.
143 | 
144 | ```php
145 | $finder = \XF::finder('XF:User');
146 | $users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);
147 | ```
148 | 
149 | #### limit method
150 | 
151 | We've already seen how to limit a query to a specific number of records being returned:
152 | 
153 | ```php
154 | $finder = \XF::finder('XF:User');
155 | $users = $finder->limit(10)->fetch();
156 | ```
157 | 
158 | However, there's actually an alternative to calling the limit method directly:
159 | 
160 | ```php
161 | $finder = \XF::finder('XF:User');
162 | $users = $finder->fetch(10);
163 | ```
164 | 
165 | It's possible to pass your limit directly into the `fetch()` method. It's also worth noting that the `limit` (and `fetch`) method supports two arguments. The first obviously being the limit, the second being the offset.
166 | 
167 | ```php
168 | $finder = \XF::finder('XF:User');
169 | $users = $finder->limit(10, 100)->fetch();
170 | ```
171 | 
172 | The offset value here essentially means the first 100 results will be discarded, and the first 10 after that will be returned. This kind of approach is useful for providing paginated results, though we actually also have an easier way to do that...
173 | 
174 | #### limitByPage method
175 | 
176 | This method is a sort of helper method which ultimately sets the appropriate limit and offset based on the "page" you're currently viewing and how many "per page" you require.
177 | 
178 | ```php
179 | $finder = \XF::finder('XF:User');
180 | $users = $finder->limitByPage(3, 20);
181 | ```
182 | 
183 | In this case, the limit is going to be set to 20 (which is our per page value) and the offset is going to be set to 40 because we're starting on page 3.
184 | 
185 | Occasionally, it is necessary for us to grab additional more data than the limit. Over-fetching can be useful to help detect whether you have additional data to display after the current page, or if you have a need to filter the initial result set down based on permissions. We can do that with the third argument:
186 | 
187 | ```php
188 | $finder = \XF::finder('XF:User');
189 | $users = $finder->limitByPage(3, 20, 1);
190 | ```
191 | 
192 | This will get a total of up to **21** users (20 + 1) starting at page 3.
193 | 
194 | ### getQuery method
195 | 
196 | When you first start working with the finder, as intuitive as it is, you may occasionally wonder whether you're using it correctly, and whether it is going to build the query you expect it to. We have a method named `getQuery` which can tell us the current query that will be built with the current finder object. For example:
197 | 
198 | ```php
199 | $finder = \XF::finder('XF:User')
200 | 	->where('user_id', 1);
201 | 
202 | \XF::dumpSimple($finder->getQuery());
203 | ```
204 | 
205 | This will output something similar to:
206 | 
207 | ```title="Dump"
208 | string(67) "SELECT `xf_user`.*
209 | FROM `xf_user`
210 | WHERE (`xf_user`.`user_id` = 1)"
211 | ```
212 | 
213 | You probably won't need it very often, but it can be useful if the finder isn't quite returning the results you expected. Read more about the `dumpSimple` method in the [Dump a variable](development-tools.md#dump-a-variable) section.
214 | 
215 | ### Custom finder methods
216 | 
217 | So far we have seen the finder object get setup with an argument similar to `XF:User` and `XF:Thread`. For the most part, this identifies the Entity class the finder is working with and will resolve to, for example, `XF\Entity\User`. However, it can additionally represent a finder class. Finder classes are optional, but they serve as a way to add custom finder methods to specific finder types. To see this in action, let's look at the finder class that relates to `XF:User` which can be found in the `XF\Finder\User` class.
218 | 
219 | Here's an example finder method from that class:
220 | 
221 | ```php
222 | public function isRecentlyActive($days = 180)
223 | {
224 | 	$this->where('last_activity', '>', time() - ($days * 86400));
225 | 	return $this;
226 | }
227 | ```
228 | 
229 | What this allows us to do is to now call that method on any User finder object. So if we take an example earlier:
230 | 
231 | ```php
232 | $finder = \XF::finder('XF:User');
233 | $users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);
234 | ```
235 | 
236 | This query, which earlier just returned 10 users in descending message count order, will now return the 10 users in that order who have been recently active in the last 20 days.
237 | 
238 | Even though for a lot of entity types a finder class doesn't exist, it is still possible to extend these non existent classes in the same way as mentioned in the [Extending classes](development-tools.md#extending-classes) section.
239 | 
240 | ## The Entity system
241 | 
242 | If you're familiar with XF1, you may be familiar with some of the concepts behind Entities because they have ultimately derived from the DataWriter system there. In case you're not so familiar with them, the following section should give you some idea.
243 | 
244 | ### Entity structure
245 | 
246 | The `Structure` object consists of a number of properties which define the structure of the Entity and the database table it relates to. The structure object itself is setup inside the entity it relates to. Let's look at some of the common properties from the User entity:
247 | 
248 | #### Table
249 | 
250 | ```php
251 | $structure->table = 'xf_user';
252 | ```
253 | 
254 | This tells the Entity which database table to use when updating and inserting records, and also tells the Finder which table to read from when building queries to execute. Additionally, it plays a part in knowing which other tables your query needs to join to.
255 | 
256 | #### Short name
257 | 
258 | ```php
259 | $structure->shortName = 'XF:User';
260 | ```
261 | 
262 | This is the just the short class name of both the Entity itself and the Finder class (if applicable).
263 | 
264 | #### Content type
265 | 
266 | ```php
267 | $structure->contentType = 'user';
268 | ```
269 | 
270 | This defines what content type this Entity represents. This will not be needed in most entity structures. It is used to connect to specific things used by the "content type" system (which will be covered in another section).
271 | 
272 | #### Primary key
273 | 
274 | ```php
275 | $structure->primaryKey = 'user_id';
276 | ```
277 | 
278 | Defines the column which represents the primary key in the database table. If a table supports more than a single column as a primary key, then this can be defined as an array.
279 | 
280 | #### Columns
281 | 
282 | ```php
283 | $structure->columns = [
284 |     'user_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true, 'changeLog' => false],
285 |     'username' => ['type' => self::STR, 'maxLength' => 50,
286 |         'required' => 'please_enter_valid_name'
287 |     ]
288 |     // and many more columns ...
289 | ];
290 | ```
291 | 
292 | This is a key part of the configuration of the entity as this goes into a lot of detail to explain the specifics of each database column that the Entity is responsible for. This tells us the type of data that is expected, whether a value is required, what format it should match, whether it should be a unique value, what its default value is, and much more.
293 | 
294 | Based on the `type`, the entity manager knows whether to encode or decode a value in a certain way. This may be a somewhat simple process of casting a value to a string or an integer, or slightly more complicated such as using `json_encode()` on an array when writing to the database or using `json_decode()` on a JSON string when reading from the database so that the value is correctly returned to the entity object as an array without us needing to manually do that. It can also support comma separated values being encoded/decoded appropriately.
295 | 
296 | Occasionally it is necessary to do some additional verification or modification of a value before it is written. As an example, in the User entity, look at the `verifyStyleId()` method. When a value is set on the `style_id` field, we automatically check to see if a method named `verifyStyleId()` exists, and if it does, we run the value through that first.
297 | 
298 | #### Behaviors
299 | 
300 | ```php
301 | $structure->behaviors = [
302 |     'XF:ChangeLoggable' => []
303 | ];
304 | ```
305 | 
306 | This is an array of behavior classes which should be used by this entity. Behavior classes are a way of allowing certain code to be reused generically across multiple entity types (only when the entity changes, not on reads). A good example of this is the `XF:Likeable` behavior which is able to automatically execute certain actions on entities which support content which can be "liked". This includes automatically recalculating counts when visibility changes occur within the content and automatically deleting likes when the content is deleted.
307 | 
308 | #### Getters
309 | 
310 | ```php
311 | $structure->getters = [
312 |     'is_super_admin' => true,
313 |     'last_activity' => true
314 | ];
315 | ```
316 | 
317 | Getter methods are automatically called when the named fields are called. For example, if we request `is_super_admin` from a User entity, this will automatically check for, and use the `getIsSuperAdmin()` method. The interesting thing to note about this is that the `xf_user` table doesn't actually have a field named `is_super_admin`. This actually exists on the Admin entity, but we have added it as a getter method as a shorthand way of accessing that value. Getter methods can also be used to override the values of existing fields directly, which is the case for the `last_activity` value here. `last_activity` is actually a cached value which is updated usually when a user logs out. However, we store the user's latest activity date in the xf_session_activity table, so we can use this `getLastActivity` method to return that value instead of the cached last activity value. Should you ever have a need to bypass the getter method entirely, and just get the true entity value, just suffix the column name with an underscore, e.g. `$user->last_activity\_`.
318 | 
319 | Because an entity is just like any other PHP object, you can add more methods to them. A common use case for this is for adding things like permission check methods that can be called on the entity itself.
320 | 
321 | #### Relations
322 | 
323 | ```php
324 | $structure->relations = [
325 |     'Admin' => [
326 |         'entity' => 'XF:Admin',
327 |         'type' => self::TO_ONE,
328 |         'conditions' => 'user_id',
329 |         'primary' => true
330 |     ]
331 | ];
332 | ```
333 | 
334 | This is how Relations are defined. What are relations? They define the relationship between entities which can be used to perform join queries to other tables or fetch records associated to an entity on the fly. If we remember the `with` method on the finder, if we wanted to fetch a specific user and preemptively fetch the user's Admin record (if it exists) then we would do something like the following:
335 | 
336 | ```php
337 | $finder = \XF::finder('XF:User');
338 | $user = $finder->where('user_id', 1)->with('Admin')->fetchOne();
339 | ```
340 | 
341 | This will use the information defined in the user entity for the `Admin` relation and the details of the `XF:Admin` entity structure to know that this user query should perform a `LEFT JOIN` on the xf_admin table and the `user_id` column. To access the admin last login date from the user entity:
342 | 
343 | ```php
344 | $lastLogin = $user->Admin->last_login; // returns timestamp of the last admin login
345 | ```
346 | 
347 | However, it's not always necessary to do a join in a finder to get related information for an entity. For example, if we take the above example without the `with` method call:
348 | 
349 | ```php
350 | $finder = \XF::finder('XF:User');
351 | $user = $finder->where('user_id', 1)->fetchOne();
352 | $lastLogin = $user->Admin->last_login; // returns timestamp of the last admin login
353 | ```
354 | 
355 | We still get the `last_login` value here. It does this by performing the additional query to get the Admin entity on the fly.
356 | 
357 | The example above uses the `TO_ONE` type, and this relation, therefore, relates one entity to one other entity. We also have a `TO_MANY` type.
358 | 
359 | It is not possible to fetch an entire `TO_MANY` relation (e.g. with a join / `with` method on the finder), but at the cost of a query it is possible to read that at any time on the fly, such as in the final `last_login` example above.
360 | 
361 | One such relation that is defined on the User entity is the `ConnectedAccounts` relation:
362 | 
363 | ```php
364 | $structure->relations = [
365 |     'ConnectedAccounts' => [
366 |     	'entity' => 'XF:UserConnectedAccount',
367 |     	'type' => self::TO_MANY,
368 |     	'conditions' => 'user_id',
369 |     	'key' => 'provider'
370 |     ]
371 | ];
372 | ```
373 | 
374 | This relation is able to return the records from the xf_user_connected_account table that match the current user ID as a `FinderCollection`. This is similar to the `ArrayCollection` object we mentioned in [The Finder](#the-finder) section above. The relation definition specifies that the collection should be keyed by the `provider` field.
375 | 
376 | Although it isn't possible to fetch multiple records while performing a finder query, it is possible to use a `TO_MANY` relation to fetch a **single** record from that relation. As an example, if we wanted to see if the user was associated to a specific connected account provider, we can at least fetch that while querying:
377 | 
378 | ```php
379 | $finder = \XF::finder('XF:User');
380 | $user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();
381 | ```
382 | 
383 | #### Options
384 | 
385 | ```php
386 | $structure->options = [
387 | 	'custom_title_disallowed' => preg_split('/\r?\n/', $options->disallowedCustomTitles),
388 | 	'admin_edit' => false,
389 | 	'skip_email_confirm' => false
390 | ];
391 | ```
392 | 
393 | Entity options are a way of modifying the behavior of the entity under certain conditions. For example, if we set `admin_edit` to true (which is the case when editing a user in the Admin CP), then certain checks will be skipped such as to allow a user's email address to be empty.
394 | 
395 | ### The Entity life cycle
396 | 
397 | The Entity plays a significant job in terms of managing the life cycle of a record within the database. As well as reading values from it, and writing values to it, the Entity can be used to delete records and trigger certain events when all of these actions occur so that certain tasks can be performed, or certain associated records can be updated as well. Let's look at some of these events that happen when an entity is saving:
398 | 
399 | - `_preSave()` - This happens before the save process begins, and is primarily used to perform any additional pre-save validations or to set additional data before the save happens.
400 | - `_postSave()` - After the data has been saved, but before any transactions are committed, this method is called and you can use it to perform any additional work that should trigger after an entity has been saved.
401 | 
402 | There are additionally `_preDelete()` and `_postDelete()` which work in a similar way, but when a delete is happening.
403 | 
404 | The Entity is also able to give information on its current state. For example, there is an `isInsert()` and `isUpdate()` method so you can detect whether this is a new record being inserted or an existing record being updated. There is an `isChanged()` method which can tell you whether a specific field has changed since the last save.
405 | 
406 | Let's look at some real examples of these methods in action, in the User entity.
407 | 
408 | ```php
409 |  protected function _preSave()
410 |  {
411 |  	if ($this->isChanged('user_group_id') || $this->isChanged('secondary_group_ids'))
412 |  	{
413 |  		$groupRepo = $this->getUserGroupRepo();
414 |  		$this->display_style_group_id = $groupRepo->getDisplayGroupIdForUser($this);
415 |  	}
416 | 
417 |  	// ...
418 |  }
419 | 
420 |  protected function _postSave()
421 |  {
422 |     // ...
423 | 
424 |  	if ($this->isUpdate() && $this->isChanged('username') && $this->getExistingValue('username') != null)
425 |  	{
426 |  		$this->app()->jobManager()->enqueue('XF:UserRenameCleanUp', [
427 |  			'originalUserId' => $this->user_id,
428 |  			'originalUserName' => $this->getExistingValue('username'),
429 |  			'newUserName' => $this->username
430 |  		]);
431 |  	}
432 | 
433 |  	// ...
434 | ```
435 | 
436 | In the `_preSave()` example we fetch and cache the new display group ID for a user based on their changed user groups. In the `_postSave()` example, we trigger a job to run after a user's name has been changed.
437 | 
438 | ## Repositories
439 | 
440 | Repositories are a new concept for XF2, but you might not be blamed for comparing them to the "Model" objects from XF1. We don't have a model object in XF2 because we have much better places and ways to fetch and write data to the database. So, rather than having a massive class which contains all of the queries your add-on needs, and all of the various different ways to manipulate those queries, we have the finder which adds a lot more flexibility.
441 | 
442 | It's also worth bearing in mind that in XF1 the Model objects were a bit of a "dumping ground" for so many things. Many of which are now redundant. For example, in XF1 all of the permission rebuilding code was in the permission model. In XF2, we have specific services and objects which handle this.
443 | 
444 | So, what are Repositories? They correspond with an entity and a finder and hold methods which generally return a finder object setup for a specific purpose. Why not just return the result of the finder query? Well, if we return the finder object itself then it serves as a useful extension point for add-ons to extend that and modify the finder object before the entity or collection is returned.
445 | 
446 | Repositories may also contain some specific methods for things like cache rebuilding.
447 | 


--------------------------------------------------------------------------------
/docs/files/Demo-Portal-1.0.0 Alpha.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/Demo-Portal-1.0.0 Alpha.zip


--------------------------------------------------------------------------------
/docs/files/example-sources/all-for-one-criterion-2.0.10.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/example-sources/all-for-one-criterion-2.0.10.zip


--------------------------------------------------------------------------------
/docs/files/example-sources/posts-remover-2.0.10.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/example-sources/posts-remover-2.0.10.zip


--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-awarded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-awarded.png


--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-notice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-notice.png


--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-messages-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-messages-after.png


--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-messages-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-messages-before.png


--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-remover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-remover.png


--------------------------------------------------------------------------------
/docs/files/images/example-userbanners-tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-userbanners-tag.png


--------------------------------------------------------------------------------
/docs/files/images/helper_criteria_tabs_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/helper_criteria_tabs_example.png


--------------------------------------------------------------------------------
/docs/files/images/linux-debugging.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/linux-debugging.jpg


--------------------------------------------------------------------------------
/docs/files/images/linux-php-versions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/linux-php-versions.png


--------------------------------------------------------------------------------
/docs/files/images/macos-debugging.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/macos-debugging.jpg


--------------------------------------------------------------------------------
/docs/files/images/macos-php-versions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/macos-php-versions.png


--------------------------------------------------------------------------------
/docs/files/images/server-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/server-report.png


--------------------------------------------------------------------------------
/docs/files/info.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/info.zip


--------------------------------------------------------------------------------
/docs/files/linux/install-debian.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | 
 3 | # PHP versions for Debian
 4 | sudo apt-get -y install apt-transport-https lsb-release ca-certificates curl
 5 | sudo curl -sSL -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
 6 | sudo sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
 7 | 
 8 | # TablePlus (optional)
 9 | wget -O - -q http://deb.tableplus.com/apt.tableplus.com.gpg.key | sudo apt-key add -
10 | sudo add-apt-repository "deb [arch=amd64] https://deb.tableplus.com/debian tableplus main"
11 | 
12 | # ElasticSearch (optional)
13 | wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
14 | echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
15 | 
16 | # Fetch the latest version info and upgrade existing packages
17 | sudo apt update -y
18 | sudo apt upgrade -y
19 | 
20 | # PHP stuff
21 | sudo apt install php5.6-fpm -y
22 | sudo apt install php7.4-fpm -y
23 | sudo apt install php8.0-fpm -y
24 | sudo apt install php-pear -y
25 | sudo apt install php-memcache -y
26 | 
27 | # PHP modules
28 | for module in xdebug imagick gettext gd bcmath bz2 curl dba xml gmp intl ldap mbstring mysql odbc soap zip enchant sqlite3
29 | do
30 |     for version in 7.4 5.6 8.0
31 |     do
32 |         sudo apt install php${version}-${module} -y
33 |     done
34 | done
35 | 
36 | # Apache web server
37 | sudo apt install apache2 -y
38 | # enable Apache FastCGI / FPM module
39 | sudo a2enmod proxy_fcgi
40 | 
41 | # TablePlus (optional)
42 | sudo apt install tableplus -y
43 | 
44 | # ElasticSearch (optional)
45 | sudo apt install elasticsearch -y
46 | 
47 | # MariaDB (MySQL)
48 | sudo apt install mariadb-server -y
49 | sudo mysql -uroot -p


--------------------------------------------------------------------------------
/docs/files/linux/install-ubuntu.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | 
 3 | # PHP versions for Ubuntu
 4 | sudo add-apt-repository -y ppa:ondrej/php
 5 | 
 6 | # TablePlus (optional)
 7 | wget -O - -q http://deb.tableplus.com/apt.tableplus.com.gpg.key | sudo apt-key add -
 8 | sudo add-apt-repository "deb [arch=amd64] https://deb.tableplus.com/debian tableplus main"
 9 | 
10 | # ElasticSearch (optional)
11 | wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
12 | echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
13 | 
14 | # Fetch the latest version info and upgrade existing packages
15 | sudo apt update -y
16 | sudo apt upgrade -y
17 | 
18 | # PHP stuff
19 | sudo apt install php5.6-fpm -y
20 | sudo apt install php7.4-fpm -y
21 | sudo apt install php8.0-fpm -y
22 | sudo apt install php-pear -y
23 | sudo apt install php-memcache -y
24 | 
25 | # PHP modules
26 | for module in xdebug imagick gettext gd bcmath bz2 curl dba xml gmp intl ldap mbstring mysql odbc soap zip enchant sqlite3
27 | do
28 |     for version in 7.4 5.6 8.0
29 |     do
30 |         sudo apt install php${version}-${module} -y
31 |     done
32 | done
33 | 
34 | # Apache web server
35 | sudo apt install apache2 -y
36 | # enable Apache FastCGI / FPM module
37 | sudo a2enmod proxy_fcgi
38 | 
39 | # TablePlus (optional)
40 | sudo apt install tableplus -y
41 | 
42 | # ElasticSearch (optional)
43 | sudo apt install elasticsearch -y
44 | 
45 | # MariaDB (MySQL)
46 | sudo apt install mariadb-server -y
47 | sudo mysql_secure_installation
48 | 


--------------------------------------------------------------------------------
/docs/files/linux/php56/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9056


--------------------------------------------------------------------------------
/docs/files/linux/php56/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.remote_enable = 1
3 | xdebug.remote_connect_back = 1
4 | xdebug.remote_port = 9000


--------------------------------------------------------------------------------
/docs/files/linux/php74/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9074


--------------------------------------------------------------------------------
/docs/files/linux/php74/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.mode = "debug,develop"
3 | xdebug.discover_client_host = 1
4 | xdebug.client_port = 9000


--------------------------------------------------------------------------------
/docs/files/linux/php80/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9080


--------------------------------------------------------------------------------
/docs/files/linux/php80/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.mode = "debug,develop"
3 | xdebug.discover_client_host = 1
4 | xdebug.client_port = 9000


--------------------------------------------------------------------------------
/docs/files/macos/httpd/httpd-dev.conf:
--------------------------------------------------------------------------------
 1 | User kier
 2 | Group staff
 3 | 
 4 | Listen 80
 5 | ServerName localhost
 6 | Timeout 3600
 7 | 
 8 | LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so
 9 | LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
10 | LoadModule deflate_module lib/httpd/modules/mod_deflate.so
11 | LoadModule mime_magic_module lib/httpd/modules/mod_mime_magic.so
12 | LoadModule expires_module lib/httpd/modules/mod_expires.so
13 | LoadModule proxy_module lib/httpd/modules/mod_proxy.so
14 | LoadModule proxy_http_module lib/httpd/modules/mod_proxy_http.so
15 | LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
16 | 
17 | 
18 |     DirectoryIndex index.html index.php
19 | 
20 | 
21 | 
22 |     DocumentRoot "/Users/kier/Documents/www"
23 | 
24 |     
25 |         Options Indexes FollowSymLinks
26 |         AllowOverride all
27 |         Require all granted
28 |     
29 | 
30 |     
31 |         SetHandler "proxy:fcgi://localhost:9080"
32 |     
33 | 


--------------------------------------------------------------------------------
/docs/files/macos/php56/htaccess.txt:
--------------------------------------------------------------------------------
1 | 
2 |     SetHandler "proxy:fcgi://localhost:9056"
3 | 


--------------------------------------------------------------------------------
/docs/files/macos/php56/php-dev.ini:
--------------------------------------------------------------------------------
 1 | post_max_size = 20M
 2 | upload_max_filesize = 10M
 3 | date.timezone = UTC
 4 | 
 5 | [mailhog]
 6 | smtp_port = 1025
 7 | sendmail_path = "/usr/local/bin/mhsendmail"
 8 | 
 9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.remote_enable = 1
12 | xdebug.remote_connect_back = 1
13 | xdebug.remote_port = 9000
14 | 
15 | [imagick]
16 | extension = "imagick.so"


--------------------------------------------------------------------------------
/docs/files/macos/php56/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9056


--------------------------------------------------------------------------------
/docs/files/macos/php74/htaccess.txt:
--------------------------------------------------------------------------------
1 | 
2 |     SetHandler "proxy:fcgi://localhost:9074"
3 | 


--------------------------------------------------------------------------------
/docs/files/macos/php74/php-dev.ini:
--------------------------------------------------------------------------------
 1 | post_max_size = 20M
 2 | upload_max_filesize = 10M
 3 | date.timezone = UTC
 4 | 
 5 | [mailhog]
 6 | smtp_port = 1025
 7 | sendmail_path = "/usr/local/bin/mhsendmail"
 8 | 
 9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.mode = "debug,develop"
12 | xdebug.discover_client_host = 1
13 | xdebug.client_port = 9000
14 | 
15 | [imagick]
16 | extension = "imagick.so"


--------------------------------------------------------------------------------
/docs/files/macos/php74/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9074


--------------------------------------------------------------------------------
/docs/files/macos/php80/htaccess.txt:
--------------------------------------------------------------------------------
1 | 
2 |     SetHandler "proxy:fcgi://localhost:9080"
3 | 


--------------------------------------------------------------------------------
/docs/files/macos/php80/php-dev.ini:
--------------------------------------------------------------------------------
 1 | post_max_size = 20M
 2 | upload_max_filesize = 10M
 3 | date.timezone = UTC
 4 | 
 5 | [mailhog]
 6 | smtp_port = 1025
 7 | sendmail_path = "/usr/local/bin/mhsendmail"
 8 | 
 9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.mode = "debug,develop"
12 | xdebug.discover_client_host = 1
13 | xdebug.client_port = 9000
14 | 
15 | [imagick]
16 | extension = "imagick.so"


--------------------------------------------------------------------------------
/docs/files/macos/php80/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9080


--------------------------------------------------------------------------------
/docs/files/scotchbox/Vagrantfile:
--------------------------------------------------------------------------------
 1 | # -*- mode: ruby -*-
 2 | # vi: set ft=ruby :
 3 | 
 4 | $bootstrap = <