From 15c848e137770fd04b73518d9ee173c655586f94 Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Mon, 27 Dec 2021 18:28:01 +0200 Subject: [PATCH 01/26] Improving usability --- backend-controllers-ajax.md | 114 ++- backend-controls.md | 100 +- backend-forms.md | 1736 +++++++++++++++++++---------------- backend-import-export.md | 44 +- backend-lists.md | 1030 ++++++++++++--------- backend-relations.md | 455 ++++----- backend-reorder.md | 71 +- backend-users.md | 164 ++-- backend-views-partials.md | 92 +- backend-widgets.md | 468 +++++----- 10 files changed, 2353 insertions(+), 1921 deletions(-) diff --git a/backend-controllers-ajax.md b/backend-controllers-ajax.md index 33c1f015..15c3c7e8 100644 --- a/backend-controllers-ajax.md +++ b/backend-controllers-ajax.md @@ -34,15 +34,17 @@ Each controller consists of a PHP file which resides in the the **/controllers** Controller classes must extend the `\Backend\Classes\Controller` class. As any other plugin class, controllers should belong to the [plugin namespace](../plugin/registration#namespaces). The most basic representation of a Controller used inside a Plugin looks like this: - namespace Acme\Blog\Controllers; +```php +namespace Acme\Blog\Controllers; - class Posts extends \Backend\Classes\Controller { +class Posts extends \Backend\Classes\Controller { - public function index() // <=== Action method - { + public function index() // <=== Action method + { - } } +} +``` Usually each controller implements functionality for working with a single type of data - like blog posts or categories. All backend behaviors described below assume this convention. @@ -53,67 +55,83 @@ The backend controller base class defines a number of properties that allow to c Property | Description ------------- | ------------- -**$fatalError** | allows to store a fatal exception generated in an action method in order to display it in the view. -**$user** | contains a reference to the the backend user object. -**$suppressView** | allows to prevent the view display. Can be updated in the action method or in the controller constructor. -**$params** | an array of the routed parameters. -**$action** | a name of the action method being executed in the current request. -**$publicActions** | defines an array of actions available without the backend user authentication. Can be overridden in the class definition. -**$requiredPermissions** | permissions required to view this page. Can be set in the class definition or in the controller constructor. See [users & permissions](users) for details. -**$pageTitle** | sets the page title. Can be set in the action method. -**$bodyClass** | body class property used for customizing the layout. Can be set in the controller constructor or action method. -**$guarded** | controller specific methods which cannot be called as actions. Can be extended in the controller constructor. -**$layout** | specify a custom layout for the controller views (see [layouts](#layouts) below). +`$fatalError` | allows to store a fatal exception generated in an action method in order to display it in the view. +`$user` | contains a reference to the the backend user object. +`$suppressView` | allows to prevent the view display. Can be updated in the action method or in the controller constructor. +`$params` | an array of the routed parameters. +`$action` | a name of the action method being executed in the current request. +`$publicActions` | defines an array of actions available without the backend user authentication. Can be overridden in the class definition. +`$requiredPermissions` | permissions required to view this page. Can be set in the class definition or in the controller constructor. See [users & permissions](users) for details. +`$pageTitle` | sets the page title. Can be set in the action method. +`$bodyClass` | body class property used for customizing the layout. Can be set in the controller constructor or action method. +`$guarded` | controller specific methods which cannot be called as actions. Can be extended in the controller constructor. +`$layout` | specify a custom layout for the controller views (see [layouts](#layouts) below). ## Actions, views and routing Public controller methods, called **actions** are coupled to **view files** which represent the page corresponding the action. Backend view files use PHP syntax. Example of the **index.htm** view file contents, corresponding to the **index** action method: -

Hello World

+```php +

Hello World

+``` URL of this page is made up of the author name, plugin name, controller name and action name. - backend/[author name]/[plugin name]/[controller name]/[action name] +```none +backend/[author name]/[plugin name]/[controller name]/[action name] +``` The above Controller results in the following: - http://example.com/backend/acme/blog/users/index +```none +https://example.com/backend/acme/blog/users/index +``` ## Passing data to views Use the controller's `$vars` property to pass any data directly to your view: - $this->vars['myVariable'] = 'value'; +```php +$this->vars['myVariable'] = 'value'; +``` The variables passed with the `$vars` property can now be accessed directly in your view: -

The variable value is

+```php +

The variable value is

+``` ## Setting the navigation context Plugins can register the backend navigation menus and submenus in the [plugin registration file](../plugin/registration#navigation-menus). The navigation context determines what backend menu and submenu are active for the current backend page. You can set the navigation context with the `BackendMenu` class: - BackendMenu::setContext('Acme.Blog', 'blog', 'categories'); +```php +BackendMenu::setContext('Acme.Blog', 'blog', 'categories'); +``` The first parameter specifies the author and plugin names. The second parameter sets the menu code. The optional third parameter specifies the submenu code. Usually you call the `BackendMenu::setContext` in the controller constructor. - namespace Acme\Blog\Controllers; +```php +namespace Acme\Blog\Controllers; - class Categories extends \Backend\Classes\Controller { +class Categories extends \Backend\Classes\Controller { - public function __construct() - { - parent::__construct(); +public function __construct() +{ + parent::__construct(); - BackendMenu::setContext('Acme.Blog', 'blog', 'categories'); - } + BackendMenu::setContext('Acme.Blog', 'blog', 'categories'); +} +``` You can set the title of the backend page with the `$pageTitle` property of the controller class (note that the form and list behaviors can do it for you): - $this->pageTitle = 'Blog categories'; +```php +$this->pageTitle = 'Blog categories'; +``` ## Using AJAX handlers @@ -127,30 +145,34 @@ The backend AJAX handlers can be defined in the controller class or [widgets](wi Backend AJAX handlers can return an array of data, throw an exception or redirect to another page (see [AJAX event handlers](../ajax/handlers)). You can use `$this->vars` to set variables and the controller's `makePartial` method to render a partial and return its contents as a part of the response data. - public function onOpenTemplate() - { - if (Request::input('someVar') != 'someValue') { - throw new ApplicationException('Invalid value'); - } +```php +public function onOpenTemplate() +{ + if (Request::input('someVar') != 'someValue') { + throw new ApplicationException('Invalid value'); + } - $this->vars['foo'] = 'bar'; + $this->vars['foo'] = 'bar'; - return [ - 'partialContents' => $this->makePartial('some-partial') - ]; - } + return [ + 'partialContents' => $this->makePartial('some-partial') + ]; +} +``` ### Triggering AJAX requests The AJAX request can be triggered with the data attributes API or the JavaScript API. Please see the [frontend AJAX library](../ajax/introduction) for details. The following example shows how to trigger a request with a backend button. - +```html + +``` > **NOTE**: You can specifically target the AJAX handler of a widget using a prefix `widget::onName`. See the [widget AJAX handler article](../backend/widgets#generic-ajax-handlers) for more details. diff --git a/backend-controls.md b/backend-controls.md index 5eadbb15..b93fb074 100644 --- a/backend-controls.md +++ b/backend-controls.md @@ -51,35 +51,37 @@ Note that you should use the **scoreboard-item** class for your scoreboard eleme Indicators are simple reporting element that have a title, a value and a description. You can use the `positive` and `negative` classes on the value element. [Font Autumn](http://daftspunk.github.io/Font-Autumn/) icon classes allow to add an icon before the value. -
-

Weight

-

100

-

unit: kg

-
+```html +
+

Weight

+

100

+

unit: kg

+
-
-

Comments

-

44

-

previous month: 32

-
+
+

Comments

+

44

+

previous month: 32

+
-
-

Length

-

31

-

previous: 42

-
+
+

Length

+

31

+

previous: 42

+
-
-

Latest commenter

-

John Smith

-

registered: yes

-
+
+

Latest commenter

+

John Smith

+

registered: yes

+
-
-

goal meter

-

88%

-

37 posts remain

-
+
+

goal meter

+

88%

+

37 posts remain

+
+``` ![image](https://github.com/wintercms/docs/blob/main/images/name-title-indicators.png?raw=true) {.img-responsive .frame} @@ -90,17 +92,19 @@ Indicators are simple reporting element that have a title, a value and a descrip The pie chart outputs information as a circle diagram, with optional label in the center. Example markup: -
- -
+```html +
+ +
+``` ![image](https://github.com/wintercms/docs/blob/main/images/traffic-sources.png?raw=true) {.img-responsive .frame} @@ -109,16 +113,18 @@ The pie chart outputs information as a circle diagram, with optional label in th The next example shows a bar chart markup. The **wrap-legend** class is optional, it manages the legend layout. The **data-height** and **data-full-width** attributes are optional as well. -
- -
+```html +
+ +
+``` ![image](https://github.com/wintercms/docs/blob/main/images/bar-chart.png?raw=true) {.img-responsive .frame} diff --git a/backend-forms.md b/backend-forms.md index aa5e81f7..342b89b5 100644 --- a/backend-forms.md +++ b/backend-forms.md @@ -35,14 +35,16 @@ The **Form behavior** is a controller [behavior](../services/behaviors) used for The Form behavior depends on form [field definitions](#form-fields) and a [model class](../database/model). In order to use the form behavior you should add it to the `$implement` property of the controller class. Also, the `$formConfig` class property should be defined and its value should refer to the YAML file used for configuring the behavior options. - namespace Acme\Blog\Controllers; +```php +namespace Acme\Blog\Controllers; - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.FormController']; +class Categories extends \Backend\Classes\Controller +{ + public $implement = ['Backend.Behaviors.FormController']; - public $formConfig = 'config_form.yaml'; - } + public $formConfig = 'config_form.yaml'; +} +``` > **NOTE:** Very often the form and [list behavior](lists) are used together in a same controller. @@ -51,97 +53,105 @@ The Form behavior depends on form [field definitions](#form-fields) and a [model The configuration file referred in the `$formConfig` property is defined in YAML format. The file should be placed into the controller's [views directory](controllers-ajax/#introduction). Below is an example of a typical form behavior configuration file: - # =================================== - # Form Behavior Config - # =================================== +```yaml +# =================================== +# Form Behavior Config +# =================================== - name: Blog Category - form: $/acme/blog/models/post/fields.yaml - modelClass: Acme\Blog\Post +name: Blog Category +form: $/acme/blog/models/post/fields.yaml +modelClass: Acme\Blog\Post - create: - title: New Blog Post +create: + title: New Blog Post - update: - title: Edit Blog Post +update: + title: Edit Blog Post - preview: - title: View Blog Post +preview: + title: View Blog Post +``` The following fields are required in the form configuration file: Field | Description ------------- | ------------- -**name** | the name of the object being managed by this form. -**form** | a configuration array or reference to a form field definition file, see [form fields](#form-fields). -**modelClass** | a model class name, the form data is loaded and saved against this model. +`name` | the name of the object being managed by this form. +`form` | a configuration array or reference to a form field definition file, see [form fields](#form-fields). +`modelClass` | a model class name, the form data is loaded and saved against this model. The configuration options listed below are optional. Define them if you want the form behavior to support the [Create](#form-create-page), [Update](#form-update-page) or [Preview](#form-preview-page) pages. Option | Description ------------- | ------------- -**defaultRedirect** | used as a fallback redirection page when no specific redirect page is defined. -**create** | a configuration array or reference to a config file for the Create page. -**update** | a configuration array or reference to a config file for the Update page. -**preview** | a configuration array or reference to a config file for the Preview page. +`defaultRedirect` | used as a fallback redirection page when no specific redirect page is defined. +`create` | a configuration array or reference to a config file for the Create page. +`update` | a configuration array or reference to a config file for the Update page. +`preview` | a configuration array or reference to a config file for the Preview page. ### Create page To support the Create page add the following configuration to the YAML file: - create: - title: New Blog Post - redirect: acme/blog/posts/update/:id - redirectClose: acme/blog/posts - flashSave: Post has been created! +```yaml +create: + title: New Blog Post + redirect: acme/blog/posts/update/:id + redirectClose: acme/blog/posts + flashSave: Post has been created! +``` The following configuration options are supported for the Create page: Option | Description ------------- | ------------- -**title** | a page title, can refer to a [localization string](../plugin/localization). -**redirect** | redirection page when record is saved. -**redirectClose** | redirection page when record is saved and the **close** post variable is sent with the request. -**flashSave** | flash message to display when record is saved, can refer to a [localization string](../plugin/localization). -**form** | overrides the default form fields definitions for the create page only. +`title` | a page title, can refer to a [localization string](../plugin/localization). +`redirect` | redirection page when record is saved. +`redirectClose` | redirection page when record is saved and the **close** post variable is sent with the request. +`flashSave` | flash message to display when record is saved, can refer to a [localization string](../plugin/localization). +`form` | overrides the default form fields definitions for the create page only. ### Update page To support the Update page add the following configuration to the YAML file: - update: - title: Edit Blog Post - redirect: acme/blog/posts - flashSave: Post updated successfully! - flashDelete: Post has been deleted. +```yaml +update: + title: Edit Blog Post + redirect: acme/blog/posts + flashSave: Post updated successfully! + flashDelete: Post has been deleted. +``` The following configuration options are supported for the Update page: Option | Description ------------- | ------------- -**title** | a page title, can refer to a [localization string](../plugin/localization). -**redirect** | redirection page when record is saved. -**redirectClose** | redirection page when record is saved and **close** post variable is sent with the request. -**flashSave** | flash message to display when record is saved, can refer to a [localization string](../plugin/localization). -**flashDelete** | flash message to display when record is deleted, can refer to a [localization string](../plugin/localization). -**form** | overrides the default form fields definitions for the update page only. +`title` | a page title, can refer to a [localization string](../plugin/localization). +`redirect` | redirection page when record is saved. +`redirectClose` | redirection page when record is saved and **close** post variable is sent with the request. +`flashSave` | flash message to display when record is saved, can refer to a [localization string](../plugin/localization). +`flashDelete` | flash message to display when record is deleted, can refer to a [localization string](../plugin/localization). +`form` | overrides the default form fields definitions for the update page only. ### Preview page To support the Preview page add the following configuration to the YAML file: - preview: - title: View Blog Post +```yaml +preview: + title: View Blog Post +``` The following configuration options are supported for the Preview page: Option | Description ------------- | ------------- -**title** | a page title, can refer to a [localization string](../plugin/localization). -**form** | overrides the default form fields definitions for the preview page only. +`title` | a page title, can refer to a [localization string](../plugin/localization). +`form` | overrides the default form fields definitions for the preview page only. ## Defining form fields @@ -158,44 +168,48 @@ Form fields are defined with the YAML file. The form fields configuration is use Fields can be placed in three areas, the **outside area**, **primary tabs** or **secondary tabs**. The next example shows the typical contents of a form fields definition file. - # =================================== - # Form Field Definitions - # =================================== +```yaml +# =================================== +# Form Field Definitions +# =================================== - fields: - blog_title: - label: Blog Title - description: The title for this blog +fields: + blog_title: + label: Blog Title + description: The title for this blog - published_at: - label: Published date - description: When this blog post was published - type: datepicker + published_at: + label: Published date + description: When this blog post was published + type: datepicker - [...] + [...] - tabs: - fields: - [...] +tabs: + fields: + [...] - secondaryTabs: - fields: - [...] +secondaryTabs: + fields: + [...] +``` Fields from related models can be rendered with the [Relation Widget](#widget-relation) or the [Relation Manager](relations#relationship-types). The exception is a OneToOne or morphOne related field, which must be defined as **relation[field]** and then can be specified as any other field of the model: - user_name: - label: User Name - description: The name of the user - avatar[name]: - label: Avatar - description: will be saved in the Avatar table - published_at: - label: Published date - description: When this blog post was published - type: datepicker +```yaml + user_name: + label: User Name + description: The name of the user + avatar[name]: + label: Avatar + description: will be saved in the Avatar table + published_at: + label: Published date + description: When this blog post was published + type: datepicker - [...] + [...] +``` ### Tab options @@ -204,38 +218,40 @@ For each tab definition, namely `tabs` and `secondaryTabs`, you can specify thes Option | Description ------------- | ------------- -**stretch** | specifies if this tab stretches to fit the parent height. -**defaultTab** | the default tab to assign fields to. Default: Misc. -**icons** | assign icons to tabs using tab names as the key. -**lazy** | array of tabs to be loaded dynamically when clicked. Useful for tabs that contain large amounts of content. -**cssClass** | assigns a CSS class to the tab container. -**paneCssClass** | assigns a CSS class to an individual tab pane. Value is an array, key is tab index or label, value is the CSS class. It can also be specified as a string, in which case the value will be applied to all tabs. +`stretch` | specifies if this tab stretches to fit the parent height. +`defaultTab` | the default tab to assign fields to. Default: Misc. +`icons` | assign icons to tabs using tab names as the key. +`lazy` | array of tabs to be loaded dynamically when clicked. Useful for tabs that contain large amounts of content. +`cssClass` | assigns a CSS class to the tab container. +`paneCssClass` | assigns a CSS class to an individual tab pane. Value is an array, key is tab index or label, value is the CSS class. It can also be specified as a string, in which case the value will be applied to all tabs. > **NOTE:** It is not recommended to use lazy loading on tabs with fields that are affected by triggers. - tabs: - stretch: true - defaultTab: User - cssClass: text-blue - lazy: - - Groups - paneCssClass: - 0: first-tab - 1: second-tab - icons: - User: icon-user - Groups: icon-group - - fields: - username: - type: text - label: Username - tab: User +```yaml +tabs: + stretch: true + defaultTab: User + cssClass: text-blue + lazy: + - Groups + paneCssClass: + 0: first-tab + 1: second-tab + icons: + User: icon-user + Groups: icon-group - groups: - type: relation - label: Groups - tab: Groups + fields: + username: + type: text + label: Username + tab: User + + groups: + type: relation + label: Groups + tab: Groups +``` ### Field options @@ -244,30 +260,30 @@ For each field you can specify these options (where applicable): Option | Description ------------- | ------------- -**label** | a name when displaying the form field to the user. -**type** | defines how this field should be rendered (see [Available fields types](#field-types) below). Default: text. -**span** | aligns the form field to one side. Options: auto, left, right, storm, full. Default: full. The parameter `storm` allows you to display the form as a Bootstrap grid, using the `cssClass` property, for example, `cssClass: col-xs-4`. -**size** | specifies a field size for fields that use it, for example, the textarea field. Options: tiny, small, large, huge, giant. -**placeholder** | if the field supports a placeholder value. -**comment** | places a descriptive comment below the field. -**commentAbove** | places a comment above the field. -**commentHtml** | allow HTML markup inside the comment. Options: true, false. -**default** | specify the default value for the field. For `dropdown`, `checkboxlist`, `radio` and `balloon-selector` widgets, you may specify an option key here to have it selected by default. -**defaultFrom** | takes the default value from the value of another field. -**tab** | assigns the field to a tab. -**cssClass** | assigns a CSS class to the field container. -**readOnly** | prevents the field from being modified. Options: true, false. -**disabled** | prevents the field from being modified and excludes it from the saved data. Options: true, false. -**hidden** | hides the field from the view and excludes it from the saved data. Options: true, false. -**stretch** | specifies if this field stretches to fit the parent height. -**context** | specifies what context should be used when displaying the field. Context can also be passed by using an `@` symbol in the field name, for example, `name@update`. -**dependsOn** | an array of other field names this field [depends on](#field-dependencies), when the other fields are modified, this field will update. -**trigger** | specify conditions for this field using [trigger events](#field-trigger-events). -**preset** | allows the field value to be initially set by the value of another field, converted using the [input preset converter](#field-input-preset). -**required** | places a red asterisk next to the field label to indicate it is required (make sure to setup validation on the model as this is not enforced by the form controller). -**attributes** | specify custom HTML attributes to add to the form field element. -**containerAttributes** | specify custom HTML attributes to add to the form field container. -**permissions** | the [permissions](users#users-and-permissions) that the current backend user must have in order for the field to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. +`label` | a name when displaying the form field to the user. +`type` | defines how this field should be rendered (see [Available fields types](#field-types) below). Default: `text`. +`span` | aligns the form field to one side. Options: `auto`, `left`, `right`, `storm`, `full`. Default: `full`. The parameter `storm` allows you to display the form as a Bootstrap grid, using the `cssClass` property, for example, `cssClass: col-xs-4`. +`size` | specifies a field size for fields that use it, for example, the textarea field. Options: `tiny`, `small`, `large`, `huge`, `giant`. +`placeholder` | if the field supports a placeholder value. +`comment` | places a descriptive comment below the field. +`commentAbove` | places a comment above the field. +`commentHtml` | allow HTML markup inside the comment. Options: `true`, `false`. +`default` | specify the default value for the field. For `dropdown`, `checkboxlist`, `radio` and `balloon-selector` widgets, you may specify an option key here to have it selected by default. +`defaultFrom` | takes the default value from the value of another field. +`tab` | assigns the field to a tab. +`cssClass` | assigns a CSS class to the field container. +`readOnly` | prevents the field from being modified. Options: `true`, `false`. +`disabled` | prevents the field from being modified and excludes it from the saved data. Options: `true`, `false`. +`hidden` | hides the field from the view and excludes it from the saved data. Options: `true`, `false`. +`stretch` | specifies if this field stretches to fit the parent height. +`context` | specifies what context should be used when displaying the field. Context can also be passed by using an `@` symbol in the field name, for example, `name@update`. +`dependsOn` | an array of other field names this field [depends on](#field-dependencies), when the other fields are modified, this field will update. +`trigger` | specify conditions for this field using [trigger events](#field-trigger-events). +`preset` | allows the field value to be initially set by the value of another field, converted using the [input preset converter](#field-input-preset). +`required` | places a red asterisk next to the field label to indicate it is required (make sure to setup validation on the model as this is not enforced by the form controller). +`attributes` | specify custom HTML attributes to add to the form field element. +`containerAttributes` | specify custom HTML attributes to add to the form field container. +`permissions` | the [permissions](users#users-and-permissions) that the current backend user must have in order for the field to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. ## Available field types @@ -308,30 +324,36 @@ There are various native field types that can be used for the **type** setting. `text` - renders a single line text box. This is the default type used if none is specified. - blog_title: - label: Blog Title - type: text +```yaml +blog_title: + label: Blog Title + type: text +``` ### Number `number` - renders a single line text box that takes numbers only. - your_age: - label: Your Age - type: number - step: 1 # defaults to 'any' - min: 1 # defaults to not present - max: 100 # defaults to not present +```yaml +your_age: + label: Your Age + type: number + step: 1 # defaults to 'any' + min: 1 # defaults to not present + max: 100 # defaults to not present +``` If you would like to validate this field server-side on save to ensure that it is numeric, please use the `$rules` property on your model, like so: - /** - * @var array Validation rules - */ - public $rules = [ - 'your_age' => 'numeric', - ]; +```php +/** + * @var array Validation rules + */ +public $rules = [ + 'your_age' => 'numeric', +]; +``` For more information on model validation, please visit [the documentation page](../services/validation#rule-numeric). @@ -340,39 +362,47 @@ For more information on model validation, please visit [the documentation page]( `password ` - renders a single line password field. - user_password: - label: Password - type: password +```yaml +user_password: + label: Password + type: password +``` ### Email `email` - renders a single line text box with the type of `email`, triggering an email-specialised keyboard in mobile browsers. - user_email: - label: Email Address - type: email +```yaml +user_email: + label: Email Address + type: email +``` If you would like to validate this field on save to ensure that it is a properly-formatted email address, please use the `$rules` property on your model, like so: - /** - * @var array Validation rules - */ - public $rules = [ - 'user_email' => 'email', - ]; +```php +/** + * @var array Validation rules + */ +public $rules = [ + 'user_email' => 'email', +]; +``` For more information on model validation, please visit [the documentation page](../services/validation#rule-email). ### Textarea -`textarea` - renders a multiline text box. A size can also be specified with possible values: tiny, small, large, huge, giant. +`textarea` - renders a multiline text box. A size can also be specified with possible values: `tiny`, `small`, `large`, `huge`, `giant`. - blog_contents: - label: Contents - type: textarea - size: large +```yaml +blog_contents: + label: Contents + type: textarea + size: large +``` ### Dropdown @@ -383,152 +413,184 @@ The first method defines `options` directly in the YAML file(two variants): (value only): - status_type: - label: Blog Post Status - type: dropdown - default: published - options: - draft - published - archived +```yaml +status_type: + label: Blog Post Status + type: dropdown + default: published + options: + draft + published + archived +``` (key / value): - status_type: - label: Blog Post Status - type: dropdown - default: published - options: - draft: Draft - published: Published - archived: Archived +```yaml +status_type: + label: Blog Post Status + type: dropdown + default: published + options: + draft: Draft + published: Published + archived: Archived +``` The second method defines options with a method declared in the model class. If the options element is omitted, the framework expects a method with the name `get*FieldName*Options` to be defined in the model. Using the example above, the model should have the `getStatusTypeOptions` method. The first argument of this method is the current value of this field and the second is the current data object for the entire form. This method should return an array of options in the format **key => label**. - status_type: - label: Blog Post Status - type: dropdown +```yaml +status_type: + label: Blog Post Status + type: dropdown +``` Supplying the dropdown options in the model class: - public function getStatusTypeOptions($value, $formData) - { - return ['all' => 'All', ...]; - } +```php +public function getStatusTypeOptions($value, $formData) +{ + return ['all' => 'All', ...]; +} +``` The third global method `getDropdownOptions` can also be defined in the model, this will be used for all dropdown field types for the model. The first argument of this method is the field name, the second is the current value of the field, and the third is the current data object for the entire form. It should return an array of options in the format **key => label**. - public function getDropdownOptions($fieldName, $value, $formData) - { - if ($fieldName == 'status') { - return ['all' => 'All', ...]; - } - else { - return ['' => '-- none --']; - } +```php +public function getDropdownOptions($fieldName, $value, $formData) +{ + if ($fieldName == 'status') { + return ['all' => 'All', ...]; + } + else { + return ['' => '-- none --']; } +} +``` The fourth method uses a specific method declared in the model class. In the next example the `listStatuses` method should be defined in the model class. This method receives all the same arguments as the `getDropdownOptions` method, and should return an array of options in the format **key => label**. - status: - label: Blog Post Status - type: dropdown - options: listStatuses +```yaml +status: + label: Blog Post Status + type: dropdown + options: listStatuses +``` Supplying the dropdown options to the model class: - public function listStatuses($fieldName, $value, $formData) - { - return ['published' => 'Published', ...]; - } +```php +public function listStatuses($fieldName, $value, $formData) +{ + return ['published' => 'Published', ...]; +} +``` The fifth method allows you to specify a static method on a class to return the options: - status: - label: Blog Post Status - type: dropdown - options: \MyAuthor\MyPlugin\Classes\FormHelper::staticMethodOptions +```yaml +status: + label: Blog Post Status + type: dropdown + options: \MyAuthor\MyPlugin\Classes\FormHelper::staticMethodOptions +``` Supplying the dropdown options to the model class: - public static function staticMethodOptions($formWidget, $formField) - { - return ['published' => 'Published', ...]; - } +```php +public static function staticMethodOptions($formWidget, $formField) +{ + return ['published' => 'Published', ...]; +} +``` The sixth method allows you to specify a callable object via an array definition. If using PHP, you're able to provide an array with the first element being the object and the second element being the method you want to call on that object. If you're using YAML, you're limited to a static method defined as the second element and the namespaced reference to a class as the first element: - status: - label: Blog Post Status - type: dropdown - options: [\MyAuthor\MyPlugin\Classes\FormHelper, staticMethodOptions] +```yaml +status: + label: Blog Post Status + type: dropdown + options: [\MyAuthor\MyPlugin\Classes\FormHelper, staticMethodOptions] +``` Supplying the dropdown options to the model class: - public static function staticMethodOptions($formWidget, $formField) - { - return ['published' => 'Published', ...]; - } +```php +public static function staticMethodOptions($formWidget, $formField) +{ + return ['published' => 'Published', ...]; +} +``` ### Add icon to dropdown options In order to add an icon or an image for every option which will be rendered in the dropdown field the options have to be provided as a multidimensional array with the following format `'key' => ['label-text', 'icon-class'],`. ```php - public function listStatuses($fieldName, $value, $formData) - { - return [ - 'published' => ['Published', 'icon-check-circle'], - 'unpublished' => ['Unpublished', 'icon-minus-circle'], - 'draft' => ['Draft', 'icon-clock-o'] - ]; - } +public function listStatuses($fieldName, $value, $formData) +{ + return [ + 'published' => ['Published', 'icon-check-circle'], + 'unpublished' => ['Unpublished', 'icon-minus-circle'], + 'draft' => ['Draft', 'icon-clock-o'] + ]; +} ``` To define the behavior when there is no selection, you may specify an `emptyOption` value to include an empty option that can be reselected. - status: - label: Blog Post Status - type: dropdown - emptyOption: -- no status -- +```yaml +status: + label: Blog Post Status + type: dropdown + emptyOption: -- no status -- +``` Alternatively you may use the `placeholder` option to use a "one-way" empty option that cannot be reselected. - status: - label: Blog Post Status - type: dropdown - placeholder: -- select a status -- +```yaml +status: + label: Blog Post Status + type: dropdown + placeholder: -- select a status -- +``` By default the dropdown has a searching feature, allowing quick selection of a value. This can be disabled by setting the `showSearch` option to `false`. - status: - label: Blog Post Status - type: dropdown - showSearch: false +```yaml +status: + label: Blog Post Status + type: dropdown + showSearch: false +``` ### Radio List `radio` - renders a list of radio options, where only one item can be selected at a time. - security_level: - label: Access Level - type: radio - default: guests - options: - all: All - registered: Registered only - guests: Guests only +```yaml +security_level: + label: Access Level + type: radio + default: guests + options: + all: All + registered: Registered only + guests: Guests only +``` Radio lists can also support a secondary description. - security_level: - label: Access Level - type: radio - options: - all: [All, Guests and customers will be able to access this page.] - registered: [Registered only, Only logged in member will be able to access this page.] - guests: [Guests only, Only guest users will be able to access this page.] +```yaml +security_level: + label: Access Level + type: radio + options: + all: [All, Guests and customers will be able to access this page.] + registered: [Registered only, Only logged in member will be able to access this page.] + guests: [Guests only, Only guest users will be able to access this page.] +``` Radio lists support the same methods for defining the options as the [dropdown field type](#field-dropdown). For radio lists the method could return either the simple array: **key => value** or an array of arrays for providing the descriptions: **key => [label, description]**. Options can be displayed inline with each other instead of in separate rows by specifying `cssClass: 'inline-options'` on the radio field config. @@ -537,13 +599,15 @@ Radio lists support the same methods for defining the options as the [dropdown f `balloon-selector` - renders a list, where only one item can be selected at a time. - gender: - label: Gender - type: balloon-selector - default: female - options: - female: Female - male: Male +```yaml +gender: + label: Gender + type: balloon-selector + default: female + options: + female: Female + male: Male +``` Balloon selectors support the same methods for defining the options as the [dropdown field type](#field-dropdown). @@ -552,27 +616,31 @@ Balloon selectors support the same methods for defining the options as the [drop `checkbox` - renders a single checkbox. - show_content: - label: Display content - type: checkbox - default: true +```yaml +show_content: + label: Display content + type: checkbox + default: true +``` ### Checkbox List `checkboxlist` - renders a list of checkboxes. - permissions: - label: Permissions - type: checkboxlist - # set to true to explicitly enable the "Select All", "Select None" options - # on lists that have <=10 items (>10 automatically enables it) - quickselect: true - default: open_account - options: - open_account: Open account - close_account: Close account - modify_account: Modify account +```yaml +permissions: + label: Permissions + type: checkboxlist + # set to true to explicitly enable the "Select All", "Select None" options + # on lists that have <=10 items (>10 automatically enables it) + quickselect: true + default: open_account + options: + open_account: Open account + close_account: Close account + modify_account: Modify account +``` Checkbox lists support the same methods for defining the options as the [dropdown field type](#field-dropdown) and also support secondary descriptions, found in the [radio field type](#field-radio). Options can be displayed inline with each other instead of in separate rows by specifying `cssClass: 'inline-options'` on the checkboxlist field config. @@ -581,49 +649,59 @@ Checkbox lists support the same methods for defining the options as the [dropdow `switch` - renders a switchbox. - show_content: - label: Display content - type: switch - comment: Flick this switch to display content - on: myauthor.myplugin::lang.models.mymodel.show_content.on - off: myauthor.myplugin::lang.models.mymodel.show_content.off +```yaml +show_content: + label: Display content + type: switch + comment: Flick this switch to display content + on: myauthor.myplugin::lang.models.mymodel.show_content.on + off: myauthor.myplugin::lang.models.mymodel.show_content.off +``` ### Section `section` - renders a section heading and subheading. The `label` and `comment` values are optional and contain the content for the heading and subheading. - user_details_section: - label: User details - type: section - comment: This section contains details about the user. +```yaml +user_details_section: + label: User details + type: section + comment: This section contains details about the user. +``` ### Partial `partial` - renders a partial, the `path` value can refer to a partial view file otherwise the field name is used as the partial name. Inside the partial these variables are available: `$value` is the default field value, `$model` is the model used for the field and `$field` is the configured class object `Backend\Classes\FormField`. - content: - type: partial - path: $/acme/blog/models/comments/_content_field.htm +```yaml +content: + type: partial + path: $/acme/blog/models/comments/_content_field.htm +``` ### Hint `hint` - identical to a `partial` field but renders inside a hint container that can be hidden by the user. - content: - type: hint - path: content_field +```yaml +content: + type: hint + path: content_field +``` ### Widget `widget` - renders a custom form widget, the `type` field can refer directly to the class name of the widget or the registered alias name. - blog_content: - type: Backend\FormWidgets\RichEditor - size: huge +```yaml +blog_content: + type: Backend\FormWidgets\RichEditor + size: huge +``` ## Form widgets @@ -652,54 +730,64 @@ There are various form widgets included as standard, although it is common for p `codeeditor` - renders a plaintext editor for formatted code or markup. Note the options may be inherited by the code editor preferences defined for the Administrator in the backend. - css_content: - type: codeeditor - size: huge - language: html +```yaml +css_content: + type: codeeditor + size: huge + language: html +``` Option | Description ------------- | ------------- -**language** | code language, for example, php, css, javascript, html. Default: php. -**showGutter** | shows a gutter with line numbers. Default: true. -**wrapWords** | breaks long lines on to a new line. Default true. -**fontSize** | the text font size. Default: 12. +`language` | code language, for example, php, css, javascript, html. Default: `php`. +`showGutter` | shows a gutter with line numbers. Default: `true`. +`wrapWords` | breaks long lines on to a new line. Default `true`. +`fontSize` | the text font size. Default: 12. ### Color picker `colorpicker` - renders controls to select a color value. - color: - label: Background - type: colorpicker +```yaml +color: + label: Background + type: colorpicker +``` Option | Description ------------- | ------------- -**availableColors** | list of available colors. If not provided, the widget will use the global available colors. -**allowCustom** | If `false`, only colors specified in `availableColors` will be available for selection. The color picker palette selector will be disabled. Default: `true` -**allowEmpty** | allows empty input value. Default: `false` -**formats** | Specifies the color format(s) to store. Can be a string or an array of values out of `cmyk`, `hex`, `hsl` and `rgb`. Specifying `all` as a string will allow all formats. Default: `hex` -**showAlpha** | If `true`, the opacity slider will be available. Default: `false` +`availableColors` | list of available colors. If not provided, the widget will use the global available colors. +`allowCustom` | If `false`, only colors specified in `availableColors` will be available for selection. The color picker palette selector will be disabled. Default: `true` +`allowEmpty` | allows empty input value. Default: `false` +`formats` | Specifies the color format(s) to store. Can be a string or an array of values out of `cmyk`, `hex`, `hsl` and `rgb`. Specifying `all` as a string will allow all formats. Default: `hex` +`showAlpha` | If `true`, the opacity slider will be available. Default: `false` There are two ways to provide the available colors for the colorpicker. The first method defines the `availableColors` directly as a list of color codes in the YAML file: - color: - label: Background - type: colorpicker - availableColors: ['#000000', '#111111', '#222222'] +```yaml +color: + label: Background + type: colorpicker + availableColors: ['#000000', '#111111', '#222222'] +``` The second method uses a specific method declared in the model class. This method should return an array of colors in the same format as in the example above. The first argument of this method is the field name, the second is the currect value of the field, and the third is the current data object for the entire form. - color: - label: Background - type: colorpicker - availableColors: myColorList +```yaml +color: + label: Background + type: colorpicker + availableColors: myColorList +``` Supplying the available colors in the model class: - public function myColorList($fieldName, $value, $formData) - { - return ['#000000', '#111111', '#222222'] - } +```php +public function myColorList($fieldName, $value, $formData) +{ + return ['#000000', '#111111', '#222222'] +} +``` If the `availableColors` field in not defined in the YAML file, the colorpicker uses a set of 20 default colors. You can also define a custom set of default colors to be used in all color pickers that do not have the `availableColors` field specified. This can be managed in the **Customize back-end** area of the Settings. @@ -710,21 +798,23 @@ If the `availableColors` field in not defined in the YAML file, the colorpicker > **NOTE:** In order to use this with a model, the field should be defined as a `jsonable` attribute, or as another attribute that can handle storing arrayed data. - data: - type: datatable - adding: true - btnAddRowLabel: Add Row Above - btnAddRowBelowLabel: Add Row Below - btnDeleteRowLabel: Delete Row - columns: [] - deleting: true - dynamicHeight: true - fieldName: null - height: false - keyFrom: id - recordsPerPage: false - searching: false - toolbar: [] +```yaml +data: + type: datatable + adding: true + btnAddRowLabel: Add Row Above + btnAddRowBelowLabel: Add Row Below + btnDeleteRowLabel: Delete Row + columns: [] + deleting: true + dynamicHeight: true + fieldName: null + height: false + keyFrom: id + recordsPerPage: false + searching: false + toolbar: [] +``` #### Table configuration @@ -732,20 +822,20 @@ The following lists the configuration values of the data table widget itself. Option | Description ------ | ----------- -**adding** | allow records to be added to the data table. Default: `true`. -**btnAddRowLabel** | defines a custom label for the "Add Row Above" button. -**btnAddRowBelowLabel** | defines a custom label for the "Add Row Below" button. -**btnDeleteRowLabel** | defines a custom label for the "Delete Row" button. -**columns** | an array representing the column configuration of the data table. See the *Column configuration* section below. -**deleting** | allow records to be deleted from the data table. Default: `false`. -**dynamicHeight** | if `true`, the data table's height will extend or shrink depending on the records added, up to the maximum size defined by the `height` configuration value. Default: `false`. -**fieldName** | defines a custom field name to use in the POST data sent from the data table. Leave blank to use the default field alias. -**height** | the data table's height, in pixels. If set to `false`, the data table will stretch to fit the field container. -**keyFrom** | the data attribute to use for keying each record. This should usually be set to `id`. Only supports integer values. -**postbackHandlerName** | specifies the AJAX handler name in which the data table content will be sent with. When set to `null` (default), the handler name will be auto-detected from the request name used by the form which contains the data table. It is recommended to keep this as `null`. -**recordsPerPage** | the number of records to show per page. If set to `false`, the pagination will be disabled. -**searching** | allow records to be searched via a search box. Default: `false`. -**toolbar** | an array representing the toolbar configuration of the data table. +`adding` | allow records to be added to the data table. Default: `true`. +`btnAddRowLabel` | defines a custom label for the "Add Row Above" button. +`btnAddRowBelowLabel` | defines a custom label for the "Add Row Below" button. +`btnDeleteRowLabel` | defines a custom label for the "Delete Row" button. +`columns` | an array representing the column configuration of the data table. See the *Column configuration* section below. +`deleting` | allow records to be deleted from the data table. Default: `false`. +`dynamicHeight` | if `true`, the data table's height will extend or shrink depending on the records added, up to the maximum size defined by the `height` configuration value. Default: `false`. +`fieldName` | defines a custom field name to use in the POST data sent from the data table. Leave blank to use the default field alias. +`height` | the data table's height, in pixels. If set to `false`, the data table will stretch to fit the field container. +`keyFrom` | the data attribute to use for keying each record. This should usually be set to `id`. Only supports integer values. +`postbackHandlerName` | specifies the AJAX handler name in which the data table content will be sent with. When set to `null` (default), the handler name will be auto-detected from the request name used by the form which contains the data table. It is recommended to keep this as `null`. +`recordsPerPage` | the number of records to show per page. If set to `false`, the pagination will be disabled. +`searching` | allow records to be searched via a search box. Default: `false`. +`toolbar` | an array representing the toolbar configuration of the data table. #### Column configuration @@ -753,26 +843,27 @@ The data table widget allows for the specification of columns as an array via th Example: - columns: - id: - type: string - title: ID - validation: - integer: - message: Please enter a number - name: - type: string - title: Name - +```yaml +columns: + id: + type: string + title: ID + validation: + integer: + message: Please enter a number + name: + type: string + title: Name +``` Option | Description ------ | ----------- -**type** | the input type for this column's cells. Must be one of the following: `string`, `checkbox`, `dropdown` or `autocomplete`. -**options** | for `dropdown` and `autocomplete` columns only - this specifies the AJAX handler that will return the available options, as an array. The array key is used as the value of the option, and the array value is used as the option label. -**readOnly** | whether this column is read-only. Default: `false`. -**title** | defines the column's title. -**validation** | an array specifying the validation for the content of the column's cells. See the *Column validation* section below. -**width** | defines the width of the column, in pixels. +`type` | the input type for this column's cells. Must be one of the following: `string`, `checkbox`, `dropdown` or `autocomplete`. +`options` | for `dropdown` and `autocomplete` columns only - this specifies the AJAX handler that will return the available options, as an array. The array key is used as the value of the option, and the array value is used as the option label. +`readOnly` | whether this column is read-only. Default: `false`. +`title` | defines the column's title. +`validation` | an array specifying the validation for the content of the column's cells. See the *Column validation* section below. +`width` | defines the width of the column, in pixels. #### Column validation @@ -780,65 +871,69 @@ Column cells can be validated against the below types of validation. Validation Validation | Description ---------- | ----------- -**float** | Validates the data as a float. An optional boolean `allowNegative` attribute can be provided, allowing for negative float numbers. -**integer** | Validates the data as an integer. An optional boolean `allowNegative` attribute can be provided, allowing for negative integers. -**length** | Validates the data to be of a certain length. An integer `min` and `max` attribute must be provided, representing the minimum and maximum number of characters that must be entered. -**regex** | Validates the data against a regular expression. A string `pattern` attribute must be provided, defining the regular expression to test the data against. -**required** | Validates that the data must be entered before saving. +`float` | Validates the data as a float. An optional boolean `allowNegative` attribute can be provided, allowing for negative float numbers. +`integer` | Validates the data as an integer. An optional boolean `allowNegative` attribute can be provided, allowing for negative integers. +`length` | Validates the data to be of a certain length. An integer `min` and `max` attribute must be provided, representing the minimum and maximum number of characters that must be entered. +`regex` | Validates the data against a regular expression. A string `pattern` attribute must be provided, defining the regular expression to test the data against. +`required` | Validates that the data must be entered before saving. ### Date picker `datepicker` - renders a text field used for selecting date and times. - published_at: - label: Published - type: datepicker - mode: date +```yaml +published_at: + label: Published + type: datepicker + mode: date +``` Option | Description ------------- | ------------- -**mode** | the expected result, either date, datetime or time. Default: datetime. -**format** | provide an explicit date display format. Eg: Y-m-d -**minDate** | the minimum/earliest date that can be selected. -**maxDate** | the maximum/latest date that can be selected. -**firstDay** | the first day of the week. Default: 0 (Sunday). -**showWeekNumber** | show week numbers at head of row. Default: false -**ignoreTimezone** | store date and time exactly as it is displayed, ignoring the backend specified timezone preference. +`mode` | the expected result, either date, datetime or time. Default: `datetime`. +`format` | provide an explicit date display format. Eg: `Y-m-d` +`minDate` | the minimum/earliest date that can be selected. +`maxDate` | the maximum/latest date that can be selected. +`firstDay` | the first day of the week. Default: 0 (Sunday). +`showWeekNumber` | show week numbers at head of row. Default: `false` +`ignoreTimezone` | store date and time exactly as it is displayed, ignoring the backend specified timezone preference. ### File upload `fileupload` - renders a file uploader for images or regular files. - avatar: - label: Avatar - type: fileupload - mode: image - imageHeight: 260 - imageWidth: 260 - thumbOptions: - mode: crop - offset: - - 0 - - 0 - quality: 90 - sharpen: 0 - interlace: false - extension: auto +```yaml +avatar: + label: Avatar + type: fileupload + mode: image + imageHeight: 260 + imageWidth: 260 + thumbOptions: + mode: crop + offset: + - 0 + - 0 + quality: 90 + sharpen: 0 + interlace: false + extension: auto +``` Option | Description ------------- | ------------- -**mode** | the expected file type, either file or image. Default: image. -**imageWidth** | if using image type, the image will be resized to this width, optional. -**imageHeight** | if using image type, the image will be resized to this height, optional. -**fileTypes** | file extensions that are accepted by the uploader, optional. Eg: `zip,txt` -**mimeTypes** | MIME types that are accepted by the uploader, either as file extension or fully qualified name, optional. Eg: `bin,txt` -**maxFilesize** | file size in Mb that are accepted by the uploader, optional. Default: from "upload_max_filesize" param value -**useCaption** | allows a title and description to be set for the file. Default: true -**prompt** | text to display for the upload button, applies to files only, optional. -**thumbOptions** | options to pass to the thumbnail generating method for the file -**attachOnUpload** | Automatically attaches the uploaded file on upload if the parent record exists instead of using deferred binding to attach on save of the parent record. Defaults to false. +`mode` | the expected file type, either file or image. Default: image. +`imageWidth` | if using image type, the image will be resized to this width, optional. +`imageHeight` | if using image type, the image will be resized to this height, optional. +`fileTypes` | file extensions that are accepted by the uploader, optional. Eg: `zip,txt` +`mimeTypes` | MIME types that are accepted by the uploader, either as file extension or fully qualified name, optional. Eg: `bin,txt` +`maxFilesize` | file size in Mb that are accepted by the uploader, optional. Default: from "upload_max_filesize" param value +`useCaption` | allows a title and description to be set for the file. Default: true +`prompt` | text to display for the upload button, applies to files only, optional. +`thumbOptions` | options to pass to the thumbnail generating method for the file +`attachOnUpload` | Automatically attaches the uploaded file on upload if the parent record exists instead of using deferred binding to attach on save of the parent record. Defaults to false. > **NOTE:** Unlike the [Media Finder FormWidget](#widget-mediafinder), the File Upload FormWidget uses [database file attachments](../database/attachments); so the field name must match a valid `attachOne` or `attachMany` relationship on the Model associated with the Form. **IMPORTANT:** Having a database column with the name used by this field type (i.e. a database column with the name of an existing `attachOne` or `attachMany` relationship) **will** cause this FormWidget to break. Use database columns with the Media Finder FormWidget and file attachment relationships with the File Upload FormWidget. @@ -847,114 +942,123 @@ Option | Description `markdown` - renders a basic editor for markdown formatted text. - md_content: - type: markdown - size: huge - mode: split +```yaml +md_content: + type: markdown + size: huge + mode: split +``` Option | Description ------------- | ------------- -**mode** | the expected view mode, either tab or split. Default: tab. +`mode` | the expected view mode, either tab or split. Default: `tab`. ### Media finder `mediafinder` - renders a field for selecting an item from the media manager library. Expanding the field displays the media manager to locate a file. The resulting selection is a string as the relative path to the file. - background_image: - label: Background image - type: mediafinder - mode: image +```yaml +background_image: + label: Background image + type: mediafinder + mode: image +``` Option | Description ------------- | ------------- -**mode** | the expected file type, either file or image. Default: file. -**prompt** | text to display when there is no item selected. The `%s` character represents the media manager icon. -**imageWidth** | if using image type, the preview image will be displayed to this width, optional. -**imageHeight** | if using image type, the preview image will be displayed to this height, optional. +`mode` | the expected file type, either file or image. Default: file. +`prompt` | text to display when there is no item selected. The `%s` character represents the media manager icon. +`imageWidth` | if using image type, the preview image will be displayed to this width, optional. +`imageHeight` | if using image type, the preview image will be displayed to this height, optional. > **NOTE:** Unlike the [File Upload FormWidget](#widget-fileupload), the Media Finder FormWidget stores its data as a string representing the path to the image selected within the Media Library. ### Nested Form + `nestedform` - renders a nested form as the contents of this field, returns data as an array of the fields contained. > **NOTE:** In order to use this with a model, the field should be defined as a `jsonable` attribute, or as another attribute that can handle storing arrayed data. - content: - type: nestedform - usePanelStyles: false - form: - fields: - added_at: - label: Date added - type: datepicker - details: - label: Details - type: textarea - title: - label: This the title - type: text - tabs: - meta_title: - lable: Meta Title - tab: SEO - color: - label: Color - type: colorpicker - tab: Design - secondaryTabs: - is_active: - label: Active - type: checkbox - logo: - label: Logo - type: mediafinder - mode: image +```yaml +content: + type: nestedform + usePanelStyles: false + form: + fields: + added_at: + label: Date added + type: datepicker + details: + label: Details + type: textarea + title: + label: This the title + type: text + tabs: + meta_title: + lable: Meta Title + tab: SEO + color: + label: Color + type: colorpicker + tab: Design + secondaryTabs: + is_active: + label: Active + type: checkbox + logo: + label: Logo + type: mediafinder + mode: image +``` A nested form supports the same syntax as a form itself, including tabs and secondaryTabs. The jsonsable attribute, has the structure of your form definition. It's even possible to use nested forms inside a nested form. Option | Description ------------- | ------------- -**form** | same as in [form definition](#form-fields) -**usePanelStyles** | defines if a panel like look is applied or not (defaults true) +`form` | same as in [form definition](#form-fields) +`usePanelStyles` | defines if a panel like look is applied or not (defaults `true`) ### Record finder `recordfinder` - renders a field with details of a related record. Expanding the field displays a popup list to search large amounts of records. Supported by singular relationships only. - user: - label: User - type: recordfinder - list: ~/plugins/winter/user/models/user/columns.yaml - recordsPerPage: 10 - title: Find Record - prompt: Click the Find button to find a user - keyFrom: id - nameFrom: name - descriptionFrom: email - conditions: email = "bob@example.com" - scope: whereActive - searchMode: all - searchScope: searchUsers - useRelation: false - modelClass: Winter\User\Models\User +```yaml +user: + label: User + type: recordfinder + list: ~/plugins/winter/user/models/user/columns.yaml + recordsPerPage: 10 + title: Find Record + prompt: Click the Find button to find a user + keyFrom: id + nameFrom: name + descriptionFrom: email + conditions: email = "bob@example.com" + scope: whereActive + searchMode: all + searchScope: searchUsers + useRelation: false + modelClass: Winter\User\Models\User +``` Option | Description ------------- | ------------- -**keyFrom** | the name of column to use in the relation used for key. Default: id. -**nameFrom** | the column name to use in the relation used for displaying the name. Default: name. -**descriptionFrom** | the column name to use in the relation used for displaying a description. Default: description. -**title** | text to display in the title section of the popup. -**prompt** | text to display when there is no record selected. The `%s` character represents the search icon. -**list** | a configuration array or reference to a list column definition file, see [list columns](lists#list-columns). -**recordsPerPage** | records to display per page, use 0 for no pages. Default: 10 -**conditions** | specifies a raw where query statement to apply to the list model query. -**scope** | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. The first argument will contain the model that the widget will be attaching its value to, i.e. the parent model. -**searchMode** | defines the search strategy to either contain all words, any word or exact phrase. Supported options: all, any, exact. Default: all. -**searchScope** | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the search query, the first argument will contain the search term. -**useRelation** | Flag for using the name of the field as a relation name to interact with directly on the parent model. Default: `true`. Set to `false` in order to bypass the relationship logic and only store and retrieve the selected record using its primary key. Best suited for use in [`jsonable` attributes](../database/model#property-jsonable) or where the relationship is unabled to be loaded. **NOTE:** When this is disabled the field name **MUST** be the actual name of the field where the value will be stored / retrieved, it cannot be the name of a relationship. +`keyFrom` | the name of column to use in the relation used for key. Default: `id`. +`nameFrom` | the column name to use in the relation used for displaying the name. Default: `name`. +`descriptionFrom` | the column name to use in the relation used for displaying a description. Default: `description`. +`title` | text to display in the title section of the popup. +`prompt` | text to display when there is no record selected. The `%s` character represents the search icon. +`list` | a configuration array or reference to a list column definition file, see [list columns](lists#list-columns). +`recordsPerPage` | records to display per page, use 0 for no pages. Default: 10 +`conditions` | specifies a raw where query statement to apply to the list model query. +`scope` | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. The first argument will contain the model that the widget will be attaching its value to, i.e. the parent model. +`searchMode` | defines the search strategy to either contain all words, any word or exact phrase. Supported options: all, any, exact. Default: `all`. +`searchScope` | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the search query, the first argument will contain the search term. +`useRelation` | Flag for using the name of the field as a relation name to interact with directly on the parent model. Default: `true`. Set to `false` in order to bypass the relationship logic and only store and retrieve the selected record using its primary key. Best suited for use in [`jsonable` attributes](../database/model#property-jsonable) or where the relationship is unabled to be loaded. **NOTE:** When this is disabled the field name **MUST** be the actual name of the field where the value will be stored / retrieved, it cannot be the name of a relationship. **modelClass** | Class of the model to use for listing records when useRelation = false @@ -962,103 +1066,113 @@ Option | Description `relation` - renders either a dropdown or checkbox list according to the field relation type. Singular relationships display a dropdown, multiple relationships display a checkbox list. The label used for displaying each relation is sourced by the `nameFrom` or `select` definition. - categories: - label: Categories - type: relation - nameFrom: title +```yaml +categories: + label: Categories + type: relation + nameFrom: title +``` Alternatively, you may populate the label using a custom `select` statement. Any valid SQL statement works here. - user: - label: User - type: relation - select: concat(first_name, ' ', last_name) +```yaml +user: + label: User + type: relation + select: concat(first_name, ' ', last_name) +``` You can also provide a model scope to use to filter the results with the `scope` property. Option | Description ------------- | ------------- -**nameFrom** | a model attribute name used for displaying the relation label. Default: name. -**select** | a custom SQL select statement to use for the name. -**order** | an order clause to sort options by. Example: `name desc`. -**emptyOption** | text to display when there is no available selections. -**scope** | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. +`nameFrom` | a model attribute name used for displaying the relation label. Default: `name`. +`select` | a custom SQL select statement to use for the name. +`order` | an order clause to sort options by. Example: `name desc`. +`emptyOption` | text to display when there is no available selections. +`scope` | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. ### Repeater `repeater` - renders a repeating set of form fields defined within. - extra_information: - type: repeater - titleFrom: title_when_collapsed - form: - fields: - added_at: - label: Date added - type: datepicker - details: - label: Details - type: textarea - title_when_collapsed: - label: This field is the title when collapsed - type: text +```yaml +extra_information: + type: repeater + titleFrom: title_when_collapsed + form: + fields: + added_at: + label: Date added + type: datepicker + details: + label: Details + type: textarea + title_when_collapsed: + label: This field is the title when collapsed + type: text +``` Option | Description ------------- | ------------- -**form** | a reference to form field definition file, see [backend form fields](#form-fields). Inline fields can also be used. -**prompt** | text to display for the create button. Default: Add new item. -**titleFrom** | name of field within items to use as the title for the collapsed item. -**minItems** | minimum items required. Pre-displays those items when not using groups. For example if you set **'minItems: 1'** the first row will be displayed and not hidden. -**maxItems** | maximum number of items to allow within the repeater. -**groups** | references a group of form fields placing the repeater in group mode (see below). An inline definition can also be used. -**style** | the behavior style to apply for repeater items. Can be one of the following: `default`, `collapsed` or `accordion`. See the **Repeater styles** section below for more information. +`form` | a reference to form field definition file, see [backend form fields](#form-fields). Inline fields can also be used. +`prompt` | text to display for the create button. Default: `Add new item`. +`titleFrom` | name of field within items to use as the title for the collapsed item. +`minItems` | minimum items required. Pre-displays those items when not using groups. For example if you set **'minItems: 1'** the first row will be displayed and not hidden. +`maxItems` | maximum number of items to allow within the repeater. +`groups` | references a group of form fields placing the repeater in group mode (see below). An inline definition can also be used. +`style` | the behavior style to apply for repeater items. Can be one of the following: `default`, `collapsed` or `accordion`. See the **Repeater styles** section below for more information. The repeater field supports a group mode which allows a custom set of fields to be chosen for each iteration. - content: - type: repeater - prompt: Add content block - groups: $/acme/blog/config/repeater_fields.yaml +```yaml +content: + type: repeater + prompt: Add content block + groups: $/acme/blog/config/repeater_fields.yaml +``` This is an example of a group configuration file, which would be located in **/plugins/acme/blog/config/repeater_fields.yaml**. Alternatively these definitions could be specified inline with the repeater. - textarea: - name: Textarea - description: Basic text field - icon: icon-file-text-o - fields: - text_area: - label: Text Content - type: textarea - size: large - - quote: - name: Quote - description: Quote item - icon: icon-quote-right - fields: - quote_position: - span: auto - label: Quote Position - type: radio - options: - left: Left - center: Center - right: Right - quote_content: - span: auto - label: Details - type: textarea +```yaml +textarea: + name: Textarea + description: Basic text field + icon: icon-file-text-o + fields: + text_area: + label: Text Content + type: textarea + size: large + +quote: + name: Quote + description: Quote item + icon: icon-quote-right + fields: + quote_position: + span: auto + label: Quote Position + type: radio + options: + left: Left + center: Center + right: Right + quote_content: + span: auto + label: Details + type: textarea +``` Each group must specify a unique key and the definition supports the following options. Option | Description ------------- | ------------- -**name** | the name of the group. -**description** | a brief description of the group. -**icon** | defines an icon for the group, optional. -**fields** | form fields belonging to the group, see [backend form fields](#form-fields). +`name` | the name of the group. +`description` | a brief description of the group. +`icon` | defines an icon for the group, optional. +`fields` | form fields belonging to the group, see [backend form fields](#form-fields). > **NOTE**: The group key is stored along with the saved data as the `_group` attribute. @@ -1066,27 +1180,31 @@ Option | Description The `style` attribute of the repeater widget controls the behaviour of repeater items. There are three different types of styles available for developers: -- **default:** Shows all the repeater items as expanded on page load. This is the default current behavior, and will be used if style is not defined in the repeater widget's configuration. -- **collapsed:** Shows all the repeater items as collapsed (minimised) on page load. The user can collapse or expand items as they wish. -- **accordion:** Shows only the first repeater item as expanded on load, with all others collapsed. When another item is exanded, any other expanded item is collapsed, effectively making it so that only one item is expanded at a time. +- `default:` Shows all the repeater items as expanded on page load. This is the default current behavior, and will be used if style is not defined in the repeater widget's configuration. +- `collapsed:` Shows all the repeater items as collapsed (minimised) on page load. The user can collapse or expand items as they wish. +- `accordion:` Shows only the first repeater item as expanded on load, with all others collapsed. When another item is exanded, any other expanded item is collapsed, effectively making it so that only one item is expanded at a time. ### Rich editor / WYSIWYG `richeditor` - renders a visual editor for rich formatted text, also known as a WYSIWYG editor. - html_content: - type: richeditor - toolbarButtons: bold|italic - size: huge +```yaml +html_content: + type: richeditor + toolbarButtons: bold|italic + size: huge +``` Option | Description ------------- | ------------- -**toolbarButtons** | which buttons to show on the editor toolbar. +`toolbarButtons` | which buttons to show on the editor toolbar. The available toolbar buttons are: - fullscreen, bold, italic, underline, strikeThrough, subscript, superscript, fontFamily, fontSize, |, color, emoticons, inlineStyle, paragraphStyle, |, paragraphFormat, align, formatOL, formatUL, outdent, indent, quote, insertHR, -, insertLink, insertImage, insertVideo, insertAudio, insertFile, insertTable, undo, redo, clearFormatting, selectAll, html +```none +fullscreen, bold, italic, underline, strikeThrough, subscript, superscript, fontFamily, fontSize, |, color, emoticons, inlineStyle, paragraphStyle, |, paragraphFormat, align, formatOL, formatUL, outdent, indent, quote, insertHR, -, insertLink, insertImage, insertVideo, insertAudio, insertFile, insertTable, undo, redo, clearFormatting, selectAll, html +``` > **NOTE**: `|` will insert a vertical separator line in the toolbar and `-` a horizontal one. @@ -1097,54 +1215,62 @@ The available toolbar buttons are: A sensitive field that contains a previously entered value will have the value replaced with a placeholder value on load, preventing the value from being guessed by length or copied. Upon revealing the value, the original value is retrieved by AJAX and populated into the field. - api_secret: - type: sensitive - allowCopy: false - hideOnTabChange: true +```yaml +api_secret: + type: sensitive + allowCopy: false + hideOnTabChange: true +``` Option | Description ------------- | ------------- -**allowCopy** | adds a "copy" action to the sensitive field, allowing the user to copy the password without revealing it. Default: false -**hiddenPlaceholder** | sets the placeholder text that is used to simulate a hidden, unrevealed value. You can change this to a long or short string to emulate different length values. Default: `__hidden__` -**hideOnTabChange** | if true, the sensitive field will automatically be hidden if the user navigates to a different tab, or minimizes their browser. Default: true +`allowCopy` | adds a "copy" action to the sensitive field, allowing the user to copy the password without revealing it. Default: `false` +`hiddenPlaceholder` | sets the placeholder text that is used to simulate a hidden, unrevealed value. You can change this to a long or short string to emulate different length values. Default: `__hidden__` +`hideOnTabChange` | if true, the sensitive field will automatically be hidden if the user navigates to a different tab, or minimizes their browser. Default: `true` ### Tag list `taglist` - renders a field for inputting a list of tags. - tags: - type: taglist - separator: space +```yaml +tags: + type: taglist + separator: space +``` A tag list support the same methods for defining the options as the [dropdown field type](#field-dropdown). - tags: - type: taglist - options: - - Red - - Blue - - Orange +```yaml +tags: + type: taglist + options: + - Red + - Blue + - Orange +``` You may use the `mode` called **relation** where the field name is a [many-to-many relationship](../database/relations#many-to-many). This will automatically source and assign tags via the relationship. If custom tags are supported, they will be created before assignment. - tags: - type: taglist - mode: relation +```yaml +tags: + type: taglist + mode: relation +``` Option | Description ------------- | ------------- -**mode** | controls how the value is returned, either string, array or relation. Default: string -**separator** | separate tags with the specified character, either comma or space. Default: comma -**customTags** | allows custom tags to be entered manually by the user. Default: true -**options** | specifies a method or array for predefined options. Set to true to use model `get*Field*Options` method. Optional. -**nameFrom** | if relation mode is used, a model attribute name for displaying the tag name. Default: name -**useKey** | use the key instead of value for saving and reading data. Default: false +`mode` | controls how the value is returned, either string, array or relation. Default: `string` +`separator` | separate tags with the specified character, either comma or space. Default: `comma` +`customTags` | allows custom tags to be entered manually by the user. Default: true +`options` | specifies a method or array for predefined options. Set to true to use model `get*Field*Options` method. Optional. +`nameFrom` | if relation mode is used, a model attribute name for displaying the tag name. Default: `name` +`useKey` | use the key instead of value for saving and reading data. Default: `false` ## Form views -For each page your form supports [Create](#form-create-page), [Update](#form-update-page) and [Preview](#form-preview-page) you should provide a [view file](#introduction) with the corresponding name - **create.htm**, **update.htm** and **preview.htm**. +For each page your form supports [Create](#form-create-page), [Update](#form-update-page) and [Preview](#form-preview-page) you should provide a [view file](#introduction) with the corresponding name - `create.htm`, `update.htm` and `preview.htm`. The form behavior adds two methods to the controller class: `formRender` and `formRenderPreview`. These methods render the form controls configured with the YAML file described above. @@ -1153,76 +1279,82 @@ The form behavior adds two methods to the controller class: `formRender` and `fo The **create.htm** view represents the Create page that allows users to create new records. A typical Create page contains breadcrumbs, the form itself, and the form buttons. The **data-request** attribute should refer to the `onSave` AJAX handler provided by the form behavior. Below is a contents of the typical create.htm form. - 'layout']) ?> +```html +'layout']) ?> -
- formRender() ?> -
+
+ formRender() ?> +
-
-
- - - or Cancel - -
+
+
+ + + or Cancel +
+
- + +``` ### Update view -The **update.htm** view represents the Update page that allows users to update or delete existing records. A typical Update page contains breadcrumbs, the form itself, and the form buttons. The Update page is very similar to the Create page, but usually has the Delete button. The **data-request** attribute should refer to the `onSave` AJAX handler provided by the form behavior. Below is a contents of the typical update.htm form. +The **update.htm** view represents the Update page that allows users to update or delete existing records. A typical Update page contains breadcrumbs, the form itself, and the form buttons. The Update page is very similar to the Create page, but usually has the Delete button. The `data-request` attribute should refer to the `onSave` AJAX handler provided by the form behavior. Below is a contents of the typical update.htm form. - 'layout']) ?> +```html +'layout']) ?> -
- formRender() ?> -
+
+ formRender() ?> +
-
-
- - - - or Cancel - -
+
+
+ + + + or Cancel +
+
- + +``` ### Preview view The **preview.htm** view represents the Preview page that allows users to preview existing records in the read-only mode. A typical Preview page contains breadcrumbs and the form itself. Below is a contents of the typical preview.htm form. -
- formRenderPreview() ?> -
+```html +
+ formRenderPreview() ?> +
+``` ## Applying conditions to fields @@ -1236,135 +1368,149 @@ The input preset converter is defined with the `preset` [form field option](#for In this example we will automatically fill out the `url` field value when a user enters text in the `title` field. If the text **Hello world** is typed in for the Title, the URL will follow suit with the converted value of **/hello-world**. This behavior will only occur when the destination field (`url`) is empty and untouched. - title: - label: Title +```yaml +title: + label: Title - url: - label: URL - preset: - field: title - type: url +url: + label: URL + preset: + field: title + type: url +``` Alternatively, the `preset` value can also be a string that refers to the **field** only, the `type` option will then default to **slug**. - slug: - label: Slug - preset: title +```yaml +slug: + label: Slug + preset: title +``` The following options are available for the `preset` option: Option | Description ------------- | ------------- -**field** | defines the other field name to source the value from. -**type** | specifies the conversion type. See below for supported values. -**prefixInput** | optional, prefixes the converted value with the value found in the supplied input element using a CSS selector. +`field` | defines the other field name to source the value from. +`type` | specifies the conversion type. See below for supported values. +`prefixInput` | optional, prefixes the converted value with the value found in the supplied input element using a CSS selector. Following are the supported types: Type | Description ------------- | ------------- -**exact** | copies the exact value -**slug** | formats the copied value as a slug -**url** | same as slug but prefixed with a / -**camel** | formats the copied value with camelCase -**file** | formats the copied value as a file name with whitespace replaced with dashes +`exact` | copies the exact value +`slug` | formats the copied value as a slug +`url` | same as slug but prefixed with a / +`camel` | formats the copied value with camelCase +`file` | formats the copied value as a file name with whitespace replaced with dashes ### Trigger events Trigger events are defined with the `trigger` [form field option](#form-field-options) and is a simple browser based solution that uses JavaScript. It allows you to change elements attributes such as visibility or value, based on another elements' state. Here is a sample definition: - is_delayed: - label: Send later - comment: Place a tick in this box if you want to send this message at a later time. - type: checkbox - - send_at: - label: Send date - type: datepicker - cssClass: field-indent - trigger: - action: show - field: is_delayed - condition: checked +```yaml +is_delayed: + label: Send later + comment: Place a tick in this box if you want to send this message at a later time. + type: checkbox + +send_at: + label: Send date + type: datepicker + cssClass: field-indent + trigger: + action: show + field: is_delayed + condition: checked +``` In the above example the `send_at` form field will only be shown if the `is_delayed` field is checked. In other words, the field will show (action) if the other form input (field) is checked (condition). The `trigger` definition specifies these options: Option | Description ------------- | ------------- -**action** | defines the action applied to this field when the condition is met. Supported values: show, hide, enable, disable, empty. -**field** | defines the other field name that will trigger the action. Normally the field name refers to a field in the same level form. For example, if this field is in a [repeater widget](#widget-repeater), only fields in that same [repeater widget](#widget-repeater) will be checked. However, if the field name is preceded by a caret symbol `^` like: `^parent_field`, it will refer to a [repeater widget](#widget-repeater) or form one level higher than the field itself. Additionally, if more than one caret `^` is used, it will refer that many levels higher: `^^grand_parent_field`, `^^^grand_grand_parent_field`, etc. -**condition** | determines the condition the specified field should satisfy for the condition to be considered "true". Supported values: checked, unchecked, value[somevalue]. +`action` | defines the action applied to this field when the condition is met. Supported values: show, hide, enable, disable, empty. +`field` | defines the other field name that will trigger the action. Normally the field name refers to a field in the same level form. For example, if this field is in a [repeater widget](#widget-repeater), only fields in that same [repeater widget](#widget-repeater) will be checked. However, if the field name is preceded by a caret symbol `^` like: `^parent_field`, it will refer to a [repeater widget](#widget-repeater) or form one level higher than the field itself. Additionally, if more than one caret `^` is used, it will refer that many levels higher: `^^grand_parent_field`, `^^^grand_grand_parent_field`, etc. +`condition` | determines the condition the specified field should satisfy for the condition to be considered "true". Supported values: checked, unchecked, value[somevalue]. ### Field dependencies Form fields can declare dependencies on other fields by defining the `dependsOn` [form field option](#form-field-options) which provides a more robust server side solution for updating fields when their dependencies are modified. When the fields that are declared as dependencies change, the defining field will update using the AJAX framework. This provides an opportunity to interact with the field's properties using the `filterFields` methods or changing available options to be provided to the field. Examples below: - country: - label: Country - type: dropdown +```yaml +country: + label: Country + type: dropdown - state: - label: State - type: dropdown - dependsOn: country +state: + label: State + type: dropdown + dependsOn: country +``` In the above example the `state` form field will refresh when the `country` field has a changed value. When this occurs, the current form data will be filled in the model so the dropdown options can use it. - public function getCountryOptions() - { - return ['au' => 'Australia', 'ca' => 'Canada']; +```php +public function getCountryOptions() +{ + return ['au' => 'Australia', 'ca' => 'Canada']; +} + +public function getStateOptions() +{ + if ($this->country == 'au') { + return ['act' => 'Capital Territory', 'qld' => 'Queensland', ...]; } - - public function getStateOptions() - { - if ($this->country == 'au') { - return ['act' => 'Capital Territory', 'qld' => 'Queensland', ...]; - } - elseif ($this->country == 'ca') { - return ['bc' => 'British Columbia', 'on' => 'Ontario', ...]; - } + elseif ($this->country == 'ca') { + return ['bc' => 'British Columbia', 'on' => 'Ontario', ...]; } +} +``` This example is useful for manipulating the model values, but it does not have access to the form field definitions. You can filter the form fields by defining a `filterFields` method inside the model, described in the [Filtering form fields](#filter-form-fields) section. An example is provided below: - dnsprovider: - label: DNS Provider - type: dropdown - - registrar: - label: Registrar - type: dropdown - - specificfields[for][provider1]: - label: Provider 1 ID - type: text - hidden: true - dependsOn: - - dnsprovider - - registrar - - specificfields[for][provider2]: - label: Provider 2 ID - type: text - hidden: true - dependsOn: - - dnsprovider - - registrar +```yaml +dnsprovider: + label: DNS Provider + type: dropdown + +registrar: + label: Registrar + type: dropdown + +specificfields[for][provider1]: + label: Provider 1 ID + type: text + hidden: true + dependsOn: + - dnsprovider + - registrar + +specificfields[for][provider2]: + label: Provider 2 ID + type: text + hidden: true + dependsOn: + - dnsprovider + - registrar +``` And the logic for the filterFields method would be as follows: - public function filterFields($fields, $context = null) - { - $displayedVendors = strtolower($this->dnsprovider->name . $this->registrar->name); - if (str_contains($displayedVendors, 'provider1')) { - $fields->{'specificfields[for][provider1]'}->hidden = false; - } - if (str_contains($displayedVendors, 'provider2')) { - $fields->{'specificfields[for][provider2]'}->hidden = false; - } +```php +public function filterFields($fields, $context = null) +{ + $displayedVendors = strtolower($this->dnsprovider->name . $this->registrar->name); + if (str_contains($displayedVendors, 'provider1')) { + $fields->{'specificfields[for][provider1]'}->hidden = false; + } + if (str_contains($displayedVendors, 'provider2')) { + $fields->{'specificfields[for][provider2]'}->hidden = false; } +} +``` In the above example, both the `provider1` and `provider2` fields will automatically refresh whenever either the `dnsprovider` or `registrar` fields are modified. When this occurs, the full form cycle will be processed, which means that any logic defined in `filterFields` methods would be run again, allowing you to filter which fields get displayed dynamically. @@ -1373,13 +1519,15 @@ In the above example, both the `provider1` and `provider2` fields will automatic Sometimes you may need to prevent a field from being submitted. In order to do that, just add an underscore (\_) before the name of the field in the form configuration file. Form fields beginning with an underscore are purged automatically and no longer saved to the model. - address: - label: Title - type: text +```yaml +address: + label: Title + type: text - _map: - label: Point your address on the map - type: mapviewer +_map: + label: Point your address on the map + type: mapviewer +``` ## Extending form behavior @@ -1398,85 +1546,97 @@ Several controller methods can called at various points during the lifecycle of You can use your own logic for the `create`, `update` or `preview` action method in the controller, then optionally call the Form behavior parent method. - public function update($recordId, $context = null) - { - // - // Do any custom code here - // - - // Call the FormController behavior update() method - return $this->asExtension('FormController')->update($recordId, $context); - } +```php +public function update($recordId, $context = null) +{ + // + // Do any custom code here + // + + // Call the FormController behavior update() method + return $this->asExtension('FormController')->update($recordId, $context); +} +``` ### Overriding controller redirect You can specify the URL to redirect to after the model is saved by overriding the `formGetRedirectUrl` method. This method returns the location to redirect to with relative URLs being treated as backend URLs. - public function formGetRedirectUrl($context = null, $model = null) - { - return 'https://example.com'; - } +```php +public function formGetRedirectUrl($context = null, $model = null) +{ + return 'https://example.com'; +} +``` ### Extending model query The lookup query for the form [database model](../database/model) can be extended by overriding the `formExtendQuery` method inside the controller class. This example will ensure that soft deleted records can still be found and updated, by applying the **withTrashed** scope to the query: - public function formExtendQuery($query) - { - $query->withTrashed(); - } +```php +public function formExtendQuery($query) +{ + $query->withTrashed(); +} +``` ### Extending form fields You can extend the fields of another controller from outside by calling the `extendFormFields` static method on the controller class. This method can take three arguments, **$form** will represent the Form widget object, **$model** represents the model used by the form and **$context** is a string containing the form context. Take this controller for example: - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.FormController']; +```php +class Categories extends \Backend\Classes\Controller +{ + public $implement = ['Backend.Behaviors.FormController']; - public $formConfig = 'config_form.yaml'; - } + public $formConfig = 'config_form.yaml'; +} +``` Using the `extendFormFields` method you can add extra fields to any form rendered by this controller. Since this has the potential to affect all forms used by this controller, it is a good idea to check the **$model** is of the correct type. Here is an example: - Categories::extendFormFields(function($form, $model, $context) - { - if (!$model instanceof MyModel) { - return; - } +```php +Categories::extendFormFields(function($form, $model, $context) +{ + if (!$model instanceof MyModel) { + return; + } - $form->addFields([ - 'my_field' => [ - 'label' => 'My Field', - 'comment' => 'This is a custom field I have added.', - ], - ]); + $form->addFields([ + 'my_field' => [ + 'label' => 'My Field', + 'comment' => 'This is a custom field I have added.', + ], + ]); - }); +}); +``` You can also extend the form fields internally by overriding the `formExtendFields` method inside the controller class. This will only affect the form used by the `FormController` behavior. - class Categories extends \Backend\Classes\Controller - { - [...] +```php +class Categories extends \Backend\Classes\Controller +{ + [...] - public function formExtendFields($form) - { - $form->addFields([...]); - } + public function formExtendFields($form) + { + $form->addFields([...]); } +} +``` The following methods are available on the $form object. Method | Description ------------- | ------------- -**addFields** | adds new fields to the outside area -**addTabFields** | adds new fields to the tabbed area -**addSecondaryTabFields** | adds new fields to the secondary tabbed area -**removeField** | remove a field from any areas +`addFields` | adds new fields to the outside area +`addTabFields` | adds new fields to the tabbed area +`addSecondaryTabFields` | adds new fields to the secondary tabbed area +`removeField` | remove a field from any areas Each method takes an array of fields similar to the [form field configuration](#form-fields). @@ -1485,21 +1645,23 @@ Each method takes an array of fields similar to the [form field configuration](# You can filter the form field definitions by overriding the `filterFields` method inside the Model used. This allows you to manipulate visibility and other field properties based on the model data. The method takes two arguments **$fields** will represent an object of the fields already defined by the [field configuration](#form-fields) and **$context** represents the active form context. - public function filterFields($fields, $context = null) - { - if ($this->source_type == 'http') { - $fields->source_url->hidden = false; - $fields->git_branch->hidden = true; - } - elseif ($this->source_type == 'git') { - $fields->source_url->hidden = false; - $fields->git_branch->hidden = false; - } - else { - $fields->source_url->hidden = true; - $fields->git_branch->hidden = true; - } +```php +public function filterFields($fields, $context = null) +{ + if ($this->source_type == 'http') { + $fields->source_url->hidden = false; + $fields->git_branch->hidden = true; } + elseif ($this->source_type == 'git') { + $fields->source_url->hidden = false; + $fields->git_branch->hidden = false; + } + else { + $fields->source_url->hidden = true; + $fields->git_branch->hidden = true; + } +} +``` The above example will set the `hidden` flag on certain fields by checking the value of the Model attribute `source_type`. This logic will be applied when the form first loads and also when updated by a [defined field dependency](#field-dependencies). diff --git a/backend-import-export.md b/backend-import-export.md index eb89ddf5..bbb790fb 100644 --- a/backend-import-export.md +++ b/backend-import-export.md @@ -58,10 +58,10 @@ The configuration options listed below are optional. Define them if you want the Option | Description ------------- | ------------- -**defaultRedirect** | used as a fallback redirection page when no specific redirect page is defined. -**import** | a configuration array or reference to a config file for the Import page. -**export** | a configuration array or reference to a config file for the Export page. -**defaultFormatOptions** | a configuration array or reference to a config file for the default CSV format options. +`defaultRedirect` | used as a fallback redirection page when no specific redirect page is defined. +`import` | a configuration array or reference to a config file for the Import page. +`export` | a configuration array or reference to a config file for the Export page. +`defaultFormatOptions` | a configuration array or reference to a config file for the default CSV format options. ### Import page @@ -80,11 +80,11 @@ The following configuration options are supported for the Import page: Option | Description ------------- | ------------- -**title** | a page title, can refer to a [localization string](../plugin/localization). -**list** | defines the list columns available for importing. -**form** | provides additional fields used as import options, optional. -**redirect** | redirection page when the import is complete, optional -**permissions** | user permissions needed to perform the operation, optional +`title` | a page title, can refer to a [localization string](../plugin/localization). +`list` | defines the list columns available for importing. +`form` | provides additional fields used as import options, optional. +`redirect` | redirection page when the import is complete, optional +`permissions` | user permissions needed to perform the operation, optional ### Export page @@ -103,12 +103,12 @@ The following configuration options are supported for the Export page: Option | Description ------------- | ------------- -**title** | a page title, can refer to a [localization string](../plugin/localization). -**fileName** | the file name to use for the exported file, default **export.csv**. -**list** | defines the list columns available for exporting. -**form** | provides additional fields used as import options, optional. -**redirect** | redirection page when the export is complete, optional. -**useList** | set to true or the value of a list definition to enable [integration with Lists](#list-behavior-integration), default: false. +`title` | a page title, can refer to a [localization string](../plugin/localization). +`fileName` | the file name to use for the exported file, default **export.csv**. +`list` | defines the list columns available for exporting. +`form` | provides additional fields used as import options, optional. +`redirect` | redirection page when the export is complete, optional. +`useList` | set to true or the value of a list definition to enable [integration with Lists](#list-behavior-integration), default: false. ### Format options @@ -127,10 +127,10 @@ The following configuration options (all optional) are supported for the format Option | Description ------------- | ------------- -**delimiter** | Delimiter character. -**enclosure** | Enclosure character. -**escape** | Escape character. -**encoding** | File encoding (only used for the import). +`delimiter` | Delimiter character. +`enclosure` | Enclosure character. +`escape` | Escape character. +`encoding` | File encoding (only used for the import). ## Import and export views @@ -168,7 +168,7 @@ The **import.htm** view represents the Import page that allows users to import d ### Export view -The **export.htm** view represents the Export page that allows users to export a file from the database. A typical Export page contains breadcrumbs, the export section itself, and the submission buttons. The **data-request** attribute should refer to the `onExport` AJAX handler provided by the behavior. Below is a contents of the typical export.htm form. +The **export.htm** view represents the Export page that allows users to export a file from the database. A typical Export page contains breadcrumbs, the export section itself, and the submission buttons. The `data-request` attribute should refer to the `onExport` AJAX handler provided by the behavior. Below is a contents of the typical export.htm form. ```html 'layout']) ?> @@ -331,5 +331,5 @@ The following configuration options are supported: Option | Description ------------- | ------------- -**definition** | the list definition to source records from, optional. -**raw** | output the raw attribute values from the record, default: false. +`definition` | the list definition to source records from, optional. +`raw` | output the raw attribute values from the record, default: `false`. diff --git a/backend-lists.md b/backend-lists.md index a6df4dbc..b31556d3 100644 --- a/backend-lists.md +++ b/backend-lists.md @@ -30,14 +30,16 @@ The **List behavior** is a controller [behavior](../services/behaviors) used for The list behavior depends on list [column definitions](#list-columns) and a [model class](../database/model). In order to use the list behavior you should add it to the `$implement` property of the controller class. Also, the `$listConfig` class property should be defined and its value should refer to the YAML file used for configuring the behavior options. - namespace Acme\Blog\Controllers; +```php +namespace Acme\Blog\Controllers; - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.ListController']; +class Categories extends \Backend\Classes\Controller +{ + public $implement = ['Backend.Behaviors.ListController']; - public $listConfig = 'list_config.yaml'; - } + public $listConfig = 'list_config.yaml'; +} +``` > **NOTE:** Very often the list and [form behavior](../ui/form) are used together in a same controller. @@ -46,85 +48,93 @@ The list behavior depends on list [column definitions](#list-columns) and a [mod The configuration file referred in the `$listConfig` property is defined in YAML format. The file should be placed into the controller's [views directory](controllers-ajax/#introduction). Below is an example of a typical list behavior configuration file: - # =================================== - # List Behavior Config - # =================================== +```yaml +# =================================== +# List Behavior Config +# =================================== - title: Blog Posts - list: ~/plugins/acme/blog/models/post/columns.yaml - modelClass: Acme\Blog\Models\Post - recordUrl: acme/blog/posts/update/:id +title: Blog Posts +list: ~/plugins/acme/blog/models/post/columns.yaml +modelClass: Acme\Blog\Models\Post +recordUrl: acme/blog/posts/update/:id +``` The following fields are required in the list configuration file: Field | Description ------------- | ------------- -**title** | a title for this list. -**list** | a configuration array or reference to a list column definition file, see [list columns](#list-columns). -**modelClass** | a model class name, the list data is loaded from this model. +`title` | a title for this list. +`list` | a configuration array or reference to a list column definition file, see [list columns](#list-columns). +`modelClass` | a model class name, the list data is loaded from this model. The configuration options listed below are optional. Option | Description ------------- | ------------- -**filter** | filter configuration, see [filtering the list](#adding-filters). -**recordUrl** | link each list record to another page. Eg: **users/update:id**. The `:id` part is replaced with the record identifier. This allows you to link the list behavior and the [form behavior](forms). -**recordOnClick** | custom JavaScript code to execute when clicking on a record. -**noRecordsMessage** | a message to display when no records are found, can refer to a [localization string](../plugin/localization). -**deleteMessage** | a message to display when records are bulk deleted, can refer to a [localization string](../plugin/localization). -**noRecordsDeletedMessage** | a message to display when a bulk delete action is triggered, but no records were deleted, can refer to a [localization string](../plugin/localization). -**recordsPerPage** | records to display per page, use 0 for no pages. Default: 0 -**perPageOptions** | options to provide the user when selecting how many records to display per page. Default: `[20, 40, 80, 100, 120]` -**showPageNumbers** | displays page numbers with pagination. Disable this to improve list performance when working with large tables. Default: true -**toolbar** | reference to a Toolbar Widget configuration file, or an array with configuration (see below). -**showSorting** | displays the sorting link on each column. Default: true -**defaultSort** | sets a default sorting column and direction when user preference is not defined. Supports a string or an array with keys `column` and `direction`. -**showCheckboxes** | displays checkboxes next to each record. Default: false. -**showSetup** | displays the list column set up button. Default: false. -**showTree** | displays a tree hierarchy for parent/child records. Default: false. -**treeExpanded** | if tree nodes should be expanded by default. Default: false. -**customViewPath** | specify a custom view path to override partials used by the list, optional. +`filter` | filter configuration, see [filtering the list](#adding-filters). +`recordUrl` | link each list record to another page. Eg: **users/update:id**. The `:id` part is replaced with the record identifier. This allows you to link the list behavior and the [form behavior](forms). +`recordOnClick` | custom JavaScript code to execute when clicking on a record. +`noRecordsMessage` | a message to display when no records are found, can refer to a [localization string](../plugin/localization). +`deleteMessage` | a message to display when records are bulk deleted, can refer to a [localization string](../plugin/localization). +`noRecordsDeletedMessage` | a message to display when a bulk delete action is triggered, but no records were deleted, can refer to a [localization string](../plugin/localization). +`recordsPerPage` | records to display per page, use 0 for no pages. Default: 0 +`perPageOptions` | options to provide the user when selecting how many records to display per page. Default: `[20, 40, 80, 100, 120]` +`showPageNumbers` | displays page numbers with pagination. Disable this to improve list performance when working with large tables. Default: `true` +`toolbar` | reference to a Toolbar Widget configuration file, or an array with configuration (see below). +`showSorting` | displays the sorting link on each column. Default: `true` +`defaultSort` | sets a default sorting column and direction when user preference is not defined. Supports a string or an array with keys `column` and `direction`. +`showCheckboxes` | displays checkboxes next to each record. Default: `false`. +`showSetup` | displays the list column set up button. Default: `false`. +`showTree` | displays a tree hierarchy for parent/child records. Default: `false`. +`treeExpanded` | if tree nodes should be expanded by default. Default: `false`. +`customViewPath` | specify a custom view path to override partials used by the list, optional. ### Adding a toolbar To include a toolbar with the list, add the following configuration to the list configuration YAML file: - toolbar: - buttons: list_toolbar - search: - prompt: Find records +```yaml +toolbar: + buttons: list_toolbar + search: + prompt: Find records +``` The toolbar configuration allows: Option | Description ------------- | ------------- -**buttons** | a reference to a controller partial file with the toolbar buttons. Eg: **_list_toolbar.htm** -**search** | reference to a Search Widget configuration file, or an array with configuration. +`buttons` | a reference to a controller partial file with the toolbar buttons. Eg: **_list_toolbar.htm** +`search` | reference to a Search Widget configuration file, or an array with configuration. The search configuration supports the following options: Option | Description ------------- | ------------- -**prompt** | a placeholder to display when there is no active search, can refer to a [localization string](../plugin/localization). -**mode** | defines the search strategy to either contain all words, any word or exact phrase. Supported options: all, any, exact. Default: all. -**scope** | specifies a [query scope method](../database/model#query-scopes) defined in the **list model** to apply to the search query. The first argument will contain the query object (as per a regular scope method), the second will contain the search term, and the third will be an array of the columns to be searched. -**searchOnEnter** | setting this to true will make the search widget wait for the Enter key to be pressed before it starts searching (the default behavior is that it starts searching automatically after someone enters something into the search field and then pauses for a short moment). Default: false. +`prompt` | a placeholder to display when there is no active search, can refer to a [localization string](../plugin/localization). +`mode` | defines the search strategy to either contain all words, any word or exact phrase. Supported options: all, any, exact. Default: `all`. +`scope` | specifies a [query scope method](../database/model#query-scopes) defined in the **list model** to apply to the search query. The first argument will contain the query object (as per a regular scope method), the second will contain the search term, and the third will be an array of the columns to be searched. +`searchOnEnter` | setting this to true will make the search widget wait for the Enter key to be pressed before it starts searching (the default behavior is that it starts searching automatically after someone enters something into the search field and then pauses for a short moment). Default: `false`. The toolbar buttons partial referred above should contain the toolbar control definition with some buttons. The partial could also contain a [scoreboard control](../ui/scoreboard) with charts. Example of a toolbar partial with the **New Post** button referring to the **create** action provided by the [form behavior](forms): -
- New Post -
+```html +
+ New Post +
+``` ### Filtering the list To filter a list by user defined input, add the following list configuration to the YAML file: - filter: config_filter.yaml +```yaml +filter: config_filter.yaml +``` The **filter** option should make reference to a [filter configuration file](#list-filters) path or supply an array with the configuration. @@ -143,13 +153,15 @@ List columns are defined with the YAML file. The column configuration is used by The next example shows the typical contents of a list column definitions file. - # =================================== - # List Column Definitions - # =================================== +```yaml +# =================================== +# List Column Definitions +# =================================== - columns: - name: Name - email: Email +columns: + name: Name + email: Email +``` ### Column options @@ -158,22 +170,22 @@ For each column can specify these options (where applicable): Option | Description ------------- | ------------- -**label** | a name when displaying the list column to the user. -**type** | defines how this column should be rendered (see [Column types](#column-types) below). -**default** | specifies the default value for the column if value is empty. -**searchable** | include this column in the list search results. Default: false. -**invisible** | specifies if this column is hidden by default. Default: false. -**sortable** | specifies if this column can be sorted. Default: true. -**clickable** | if set to false, disables the default click behavior when the column is clicked. Default: true. -**select** | defines a custom SQL select statement to use for the value. -**valueFrom** | defines a model attribute to use for the value. -**relation** | defines a model relationship column. -**useRelationCount** | use the count of the defined `relation` as the value for this column. Default: false -**cssClass** | assigns a CSS class to the column container. -**headCssClass** | assigns a CSS class to the column header container. -**width** | sets the column width, can be specified in percents (10%) or pixels (50px). There could be a single column without width specified, it will be stretched to take the available space. -**align** | specifies the column alignment. Possible values are `left`, `right` and `center`. -**permissions** | the [permissions](users#users-and-permissions) that the current backend user must have in order for the column to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. +`label` | a name when displaying the list column to the user. +`type` | defines how this column should be rendered (see [Column types](#column-types) below). +`default` | specifies the default value for the column if value is empty. +`searchable` | include this column in the list search results. Default: `false`. +`invisible` | specifies if this column is hidden by default. Default: `false`. +`sortable` | specifies if this column can be sorted. Default: `true`. +`clickable` | if set to false, disables the default click behavior when the column is clicked. Default: `true`. +`select` | defines a custom SQL select statement to use for the value. +`valueFrom` | defines a model attribute to use for the value. +`relation` | defines a model relationship column. +`useRelationCount` | use the count of the defined `relation` as the value for this column. Default: `false` +`cssClass` | assigns a CSS class to the column container. +`headCssClass` | assigns a CSS class to the column header container. +`width` | sets the column width, can be specified in percents (10%) or pixels (50px). There could be a single column without width specified, it will be stretched to take the available space. +`align` | specifies the column alignment. Possible values are `left`, `right` and `center`. +`permissions` | the [permissions](users#users-and-permissions) that the current backend user must have in order for the column to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. ## Available column types @@ -210,31 +222,37 @@ There are various column types that can be used for the **type** setting, these `text` - displays a text column, aligned left - full_name: - label: Full Name - type: text +```yaml +full_name: + label: Full Name + type: text +``` You can also specify a custom text format, for example **Admin:Full Name (active)** - full_name: - label: Full Name - type: text - format: Admin:%s (active) +```yaml +full_name: + label: Full Name + type: text + format: Admin:%s (active) +``` ### Image `image` - displays an image using the built in [image resizing functionality](../services/image-resizing#resize-sources). - avatar: - label: Avatar - type: image - sortable: false - width: 150 - height: 150 - default: '/modules/backend/assets/images/logo.svg' - options: - quality: 80 +```yaml +avatar: + label: Avatar + type: image + sortable: false + width: 150 + height: 150 + default: '/modules/backend/assets/images/logo.svg' + options: + quality: 80 +``` See the [image resizing docs](../services/image-resizing#resize-sources) for more information on what image sources are supported and what [options](../services/image-resizing#resize-parameters) are supported @@ -243,16 +261,20 @@ See the [image resizing docs](../services/image-resizing#resize-sources) for mor `number` - displays a number column, aligned right - age: - label: Age - type: number +```yaml +age: + label: Age + type: number +``` You can also specify a custom number format, for example currency **$ 99.00** - price: - label: Price - type: number - format: $ %.2f +```yaml +price: + label: Price + type: number + format: $ %.2f +``` > **NOTE:** Both `text` and `number` columns support the `format` property, this property follows the formatting rules of the [PHP sprintf() function](https://secure.php.net/manual/en/function.sprintf.php). Value must be a string. @@ -261,32 +283,40 @@ You can also specify a custom number format, for example currency **$ 99.00** `switch` - displays a on or off state for boolean columns. - enabled: - label: Enabled - type: switch +```yaml +enabled: + label: Enabled + type: switch +``` ### Date & Time `datetime` - displays the column value as a formatted date and time. The next example displays dates as **Thu, Dec 25, 1975 2:15 PM**. - created_at: - label: Date - type: datetime +```yaml +created_at: + label: Date + type: datetime +``` You can also specify a custom date format, for example **Thursday 25th of December 1975 02:15:16 PM**: - created_at: - label: Date - type: datetime - format: l jS \of F Y h:i:s A +```yaml +created_at: + label: Date + type: datetime + format: l jS \of F Y h:i:s A +``` You may also wish to set `ignoreTimezone: true` to prevent a timezone conversion between the date that is displayed and the date stored in the database, since by default the backend timezone preference is applied to the display value. - created_at: - label: Date - type: datetime - ignoreTimezone: true +```yaml +created_at: + label: Date + type: datetime + ignoreTimezone: true +``` > **NOTE:** the `ignoreTimezone` option also applies to other date and time related field types, including `date`, `time`, `timesince` and `timetense`. @@ -295,169 +325,199 @@ You may also wish to set `ignoreTimezone: true` to prevent a timezone conversion `date` - displays the column value as date format **M j, Y** - created_at: - label: Date - type: date +```yaml +created_at: + label: Date + type: date +``` ### Time `time` - displays the column value as time format **g:i A** - created_at: - label: Date - type: time +```yaml +created_at: + label: Date + type: time +``` ### Time since `timesince` - displays a human readable time difference from the value to the current time. Eg: **10 minutes ago** - created_at: - label: Date - type: timesince +```yaml +created_at: + label: Date + type: timesince +``` ### Time tense `timetense` - displays 24-hour time and the day using the grammatical tense of the current date. Eg: **Today at 12:49**, **Yesterday at 4:00** or **18 Sep 2015 at 14:33**. - created_at: - label: Date - type: timetense +```yaml +created_at: + label: Date + type: timetense +``` ### Select `select` - allows to create a column using a custom select statement. Any valid SQL SELECT statement works here. - full_name: - label: Full Name - select: concat(first_name, ' ', last_name) +```yaml +full_name: + label: Full Name + select: concat(first_name, ' ', last_name) +``` ### Relation `relation` - allows to display related columns, you can provide a relationship option. The value of this option has to be the name of the Active Record [relationship](../database/relations) on your model. In the next example the **name** value will be translated to the name attribute found in the related model (eg: `$model->name`). - group: - label: Group - relation: groups - select: name +```yaml +group: + label: Group + relation: groups + select: name +``` To display a column that shows the number of related records, use the `useRelationCount` option. - users_count: - label: Users - relation: users - useRelationCount: true +```yaml +users_count: + label: Users + relation: users + useRelationCount: true +``` > **NOTE:** Using the `relation` option on a column will load the value from the `select`ed column into the attribute specified by this column. It is recommended that you name the column displaying the relation data without conflicting with existing model attributes as demonstrated in the examples below: **Best Practice:** - group_name: - label: Group - relation: group - select: name +```yaml +group_name: + label: Group + relation: group + select: name +``` **Poor Practice:** - # This will overwrite the value of $record->group_id which will break accessing relations from the list view - group_id: - label: Group - relation: group - select: name +```yaml +# This will overwrite the value of $record->group_id which will break accessing relations from the list view +group_id: + label: Group + relation: group + select: name +``` ### Partial `partial` - renders a partial, the `path` value can refer to a partial view file otherwise the column name is used as the partial name. Inside the partial these variables are available: `$value` is the default cell value, `$record` is the model used for the cell and `$column` is the configured class object `Backend\Classes\ListColumn`. - content: - label: Content - type: partial - path: ~/plugins/acme/blog/models/comment/_content_column.htm +```yaml +content: + label: Content + type: partial + path: ~/plugins/acme/blog/models/comment/_content_column.htm +``` ### Color Picker `colorpicker` - displays a color from colorpicker column - color: - label: Background - type: colorpicker +```yaml +color: + label: Background + type: colorpicker +``` ## Displaying the list Usually lists are displayed in the index [view](controllers-ajax/#introduction) file. Since lists include the toolbar, the view file will consist solely of the single `listRender` method call. - listRender() ?> +```php +listRender() ?> +``` ## Multiple list definitions The list behavior can support multiple lists in the same controller using named definitions. The `$listConfig` property can be defined as an array where the key is a definition name and the value is the configuration file. - public $listConfig = [ - 'templates' => 'config_templates_list.yaml', - 'layouts' => 'config_layouts_list.yaml' - ]; +```php +public $listConfig = [ + 'templates' => 'config_templates_list.yaml', + 'layouts' => 'config_layouts_list.yaml' +]; +``` Each definition can then be displayed by passing the definition name as the first argument when calling the `listRender` method: - listRender('templates') ?> +```php +listRender('templates') ?> +``` ## Using list filters Lists can be filtered by [adding a filter definition](#adding-filters) to the list configuration. Similarly filters are driven by their own configuration file that contain filter scopes, each scope is an aspect by which the list can be filtered. The next example shows a typical contents of the filter definition file. - # =================================== - # Filter Scope Definitions - # =================================== - - scopes: - - category: - label: Category - modelClass: Acme\Blog\Models\Category - conditions: category_id in (:filtered) - nameFrom: name - - status: - label: Status - type: group - conditions: status in (:filtered) - options: - pending: Pending - active: Active - closed: Closed - - published: - label: Hide published - type: checkbox - default: 1 - conditions: is_published <> true - - approved: - label: Approved - type: switch - default: 2 - conditions: - - is_approved <> true - - is_approved = true - - created_at: - label: Date - type: date - conditions: created_at >= ':filtered' - - published_at: - label: Date - type: daterange - conditions: created_at >= ':after' AND created_at <= ':before' +```yaml +# =================================== +# Filter Scope Definitions +# =================================== + +scopes: + + category: + label: Category + modelClass: Acme\Blog\Models\Category + conditions: category_id in (:filtered) + nameFrom: name + + status: + label: Status + type: group + conditions: status in (:filtered) + options: + pending: Pending + active: Active + closed: Closed + + published: + label: Hide published + type: checkbox + default: 1 + conditions: is_published <> true + + approved: + label: Approved + type: switch + default: 2 + conditions: + - is_approved <> true + - is_approved = true + + created_at: + label: Date + type: date + conditions: created_at >= ':filtered' + + published_at: + label: Date + type: daterange + conditions: created_at >= ':after' AND created_at <= ':before' +``` ### Scope options @@ -466,51 +526,55 @@ For each scope you can specify these options (where applicable): Option | Description ------------- | ------------- -**label** | a name when displaying the filter scope to the user. -**type** | defines how this scope should be rendered (see [Scope types](#scope-types) below). Default: group. -**conditions** | specifies a raw where query statement to apply to the list model query, the `:filtered` parameter represents the filtered value(s). -**scope** | specifies a [query scope method](../database/model#query-scopes) defined in the **list model** to apply to the list query. The first argument will contain the query object (as per a regular scope method) and the second argument will contain the filtered value(s) -**options** | options to use if filtering by multiple items, this option can specify an array or a method name in the `modelClass` model. -**nameFrom** | if filtering by multiple items, the attribute to display for the name, taken from all records of the `modelClass` model. -**default** | can either be integer(switch,checkbox,number) or array(group,date range,number range) or string(date). -**permissions** | the [permissions](users#users-and-permissions) that the current backend user must have in order for the filter scope to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. -**dependsOn** | a string or an array of other scope names that this scope [depends on](#filter-scope-dependencies). When the other scopes are modified, this scope will update. +`label` | a name when displaying the filter scope to the user. +`type` | defines how this scope should be rendered (see [Scope types](#scope-types) below). Default: `group`. +`conditions` | specifies a raw where query statement to apply to the list model query, the `:filtered` parameter represents the filtered value(s). +`scope` | specifies a [query scope method](../database/model#query-scopes) defined in the **list model** to apply to the list query. The first argument will contain the query object (as per a regular scope method) and the second argument will contain the filtered value(s) +`options` | options to use if filtering by multiple items, this option can specify an array or a method name in the `modelClass` model. +`nameFrom` | if filtering by multiple items, the attribute to display for the name, taken from all records of the `modelClass` model. +`default` | can either be integer(switch,checkbox,number) or array(group,date range,number range) or string(date). +`permissions` | the [permissions](users#users-and-permissions) that the current backend user must have in order for the filter scope to be used. Supports either a string for a single permission or an array of permissions of which only one is needed to grant access. +`dependsOn` | a string or an array of other scope names that this scope [depends on](#filter-scope-dependencies). When the other scopes are modified, this scope will update. ### Filter Dependencies Filter scopes can declare dependencies on other scopes by defining the `dependsOn` [scope option](#filter-scope-options), which provide a server-side solution for updating scopes when their dependencies are modified. When the scopes that are declared as dependencies change, the defining scope will update dynamically. This provides an opportunity to change the available options to be provided to the scope. - country: - label: Country - type: group - conditions: country_id in (:filtered) - modelClass: Winter\Test\Models\Location - options: getCountryOptions - - city: - label: City - type: group - conditions: city_id in (:filtered) - modelClass: Winter\Test\Models\Location - options: getCityOptions - dependsOn: country +```yaml +country: + label: Country + type: group + conditions: country_id in (:filtered) + modelClass: Winter\Test\Models\Location + options: getCountryOptions + +city: + label: City + type: group + conditions: city_id in (:filtered) + modelClass: Winter\Test\Models\Location + options: getCityOptions + dependsOn: country +``` In the above example, the `city` scope will refresh when the `country` scope has changed. Any scope that defines the `dependsOn` property will be passed all current scope objects for the Filter widget, including their current values, as an array that is keyed by the scope names. - public function getCountryOptions() - { - return Country::lists('name', 'id'); - } - - public function getCityOptions($scopes = null) - { - if (!empty($scopes['country']->value)) { - return City::whereIn('country_id', array_keys($scopes['country']->value))->lists('name', 'id'); - } else { - return City::lists('name', 'id'); - } +```php +public function getCountryOptions() +{ + return Country::lists('name', 'id'); +} + +public function getCityOptions($scopes = null) +{ + if (!empty($scopes['country']->value)) { + return City::whereIn('country_id', array_keys($scopes['country']->value))->lists('name', 'id'); + } else { + return City::lists('name', 'id'); } +} +``` > **NOTE:** Scope dependencies with `type: group` are only supported at this stage. @@ -546,41 +610,47 @@ These types can be used to determine how the filter scope should be displayed. `group` - filters the list by a group of items, usually by a related model and requires a `nameFrom` or `options` definition. Eg: Status name as open, closed, etc. - status: - label: Status - type: group - conditions: status in (:filtered) - default: - pending: Pending - active: Active - options: - pending: Pending - active: Active - closed: Closed +```yaml +status: + label: Status + type: group + conditions: status in (:filtered) + default: + pending: Pending + active: Active + options: + pending: Pending + active: Active + closed: Closed +``` ### Checkbox `checkbox` - used as a binary checkbox to apply a predefined condition or query to the list, either on or off. Use 0 for off and 1 for on for default value - published: - label: Hide published - type: checkbox - default: 1 - conditions: is_published <> true +```yaml +published: + label: Hide published + type: checkbox + default: 1 + conditions: is_published <> true +``` ### Switch `switch` - used as a switch to toggle between two predefined conditions or queries to the list, either indeterminate, on or off. Use 0 for off, 1 for indeterminate and 2 for on for default value - approved: - label: Approved - type: switch - default: 1 - conditions: - - is_approved <> true - - is_approved = true +```yaml +approved: + label: Approved + type: switch + default: 1 + conditions: + - is_approved <> true + - is_approved = true +``` ### Date @@ -591,66 +661,72 @@ These types can be used to determine how the filter scope should be displayed. - `:before`: The selected date formatted as `Y-m-d 00:00:00`, converted from the backend timezone to the app timezone - `:after`: The selected date formatted as `Y-m-d 23:59:59`, converted from the backend timezone to the app timezone - created_at: - label: Date - type: date - minDate: '2001-01-23' - maxDate: '2030-10-13' - yearRange: 10 - conditions: created_at >= ':filtered' +```yaml +created_at: + label: Date + type: date + minDate: '2001-01-23' + maxDate: '2030-10-13' + yearRange: 10 + conditions: created_at >= ':filtered' +``` ### Date Range `daterange` - displays a date picker for two dates to be selected as a date range. The values available to be used in the conditions property are: - - `:before`: The selected "before" date formatted as `Y-m-d H:i:s` - - `:beforeDate`: The selected "before" date formatted as `Y-m-d` - - `:after`: The selected "after" date formatted as `Y-m-d H:i:s` - - `:afterDate`: The selected "after" date formatted as `Y-m-d` - - published_at: - label: Date - type: daterange - minDate: '2001-01-23' - maxDate: '2030-10-13' - yearRange: 10 - conditions: created_at >= ':after' AND created_at <= ':before' +- `:before`: The selected "before" date formatted as `Y-m-d H:i:s` +- `:beforeDate`: The selected "before" date formatted as `Y-m-d` +- `:after`: The selected "after" date formatted as `Y-m-d H:i:s` +- `:afterDate`: The selected "after" date formatted as `Y-m-d` + +```yaml +published_at: + label: Date + type: daterange + minDate: '2001-01-23' + maxDate: '2030-10-13' + yearRange: 10 + conditions: created_at >= ':after' AND created_at <= ':before' +``` To use default value for Date and Date Range ```php - myController::extendListFilterScopes(function($filter) - { - 'Date Test' => [ - 'label' => 'Date Test', - 'type' => 'daterange', - 'default' => $this->myDefaultTime(), - 'conditions' => "created_at >= ':after' AND created_at <= ':before'" - ], - ]); - }); - - // return value must be instance of carbon - public function myDefaultTime() - { - return [ - 0 => Carbon::parse('2012-02-02'), - 1 => Carbon::parse('2012-04-02'), - ]; - } +myController::extendListFilterScopes(function($filter) +{ + 'Date Test' => [ + 'label' => 'Date Test', + 'type' => 'daterange', + 'default' => $this->myDefaultTime(), + 'conditions' => "created_at >= ':after' AND created_at <= ':before'" + ], + ]); +}); + +// return value must be instance of carbon +public function myDefaultTime() +{ + return [ + 0 => Carbon::parse('2012-02-02'), + 1 => Carbon::parse('2012-04-02'), + ]; +} ``` You may also wish to set `ignoreTimezone: true` to prevent a timezone conversion between the date that is displayed and the date stored in the database, since by default the backend timezone preference is applied to the display value. - published_at: - label: Date - type: daterange - minDate: '2001-01-23' - maxDate: '2030-10-13' - yearRange: 10 - conditions: created_at >= ':after' AND created_at <= ':before' - ignoreTimezone: true +```yaml +published_at: + label: Date + type: daterange + minDate: '2001-01-23' + maxDate: '2030-10-13' + yearRange: 10 + conditions: created_at >= ':after' AND created_at <= ':before' + ignoreTimezone: true +``` > **NOTE:** the `ignoreTimezone` option also applies to the `date` filter type as well. @@ -661,14 +737,16 @@ You may also wish to set `ignoreTimezone: true` to prevent a timezone conversion The `min` and `max` options specify the minimum and maximum values that can be entered by the user. The `step` option specifies the stepping interval to use when adjusting the value with the up and down arrows. - age: - label: Age - type: number - default: 14 - step: 1 - min: 0 - max: 1000 - conditions: age >= ':filtered' +```yaml +age: + label: Age + type: number + default: 14 + step: 1 + min: 0 + max: 1000 + conditions: age >= ':filtered' +``` > **NOTE:** the `step`, `min`, and `max` options also apply to the `numberrange` filter type as well. @@ -682,24 +760,28 @@ The `min` and `max` options specify the minimum and maximum values that can be e You may leave either the minimum value blank to search everything up to the maximum value, and vice versa, you may leave the maximum value blank to search everything at least the minimum value. - visitors: - label: Visitor Count - type: numberrange - default: - 0: 10 - 1: 20 - conditions: visitors >= ':min' and visitors <= ':max' +```yaml +visitors: + label: Visitor Count + type: numberrange + default: + 0: 10 + 1: 20 + conditions: visitors >= ':min' and visitors <= ':max' +``` ### Text `text` - display text input for a string to be entered. You can specify a `size` attribute that will be injected in the input size attribute (default: 10). - username: - label: Username - type: text - conditions: username = :value - size: 2 +```yaml +username: + label: Username + type: text + conditions: username = :value + size: 2 +``` ## Extending list behavior @@ -719,99 +801,113 @@ Sometimes you may wish to modify the default list behavior and there are several You can use your own logic for the `index` action method in the controller, then optionally call the List behavior `index` parent method. - public function index() - { - // - // Do any custom code here - // - - // Call the ListController behavior index() method - $this->asExtension('ListController')->index(); - } +```php +public function index() +{ + // + // Do any custom code here + // + + // Call the ListController behavior index() method + $this->asExtension('ListController')->index(); +} +``` ### Overriding views The `ListController` behavior has a main container view that you may override by creating a special file named `_list_container.htm` in your controller directory. The following example will add a sidebar to the list: - - render() ?> - - - - render() ?> - - -
-
- [Insert sidebar here] -
-
- render() ?> -
+```html + + render() ?> + + + + render() ?> + + +
+
+ [Insert sidebar here] +
+
+ render() ?>
+
+``` The behavior will invoke a `Lists` widget that also contains numerous views that you may override. This is possible by specifying a `customViewPath` option as described in the [list configuration options](#configuring-list). The widget will look in this path for a view first, then fall back to the default location. - # Custom view path - customViewPath: $/acme/blog/controllers/reviews/list +```yaml +# Custom view path +customViewPath: $/acme/blog/controllers/reviews/list +``` > **NOTE**: It is a good idea to use a sub-directory, for example `list`, to avoid conflicts. For example, to modify the list body row markup, create a file called `list/_list_body_row.htm` in your controller directory. - - $column): ?> - getColumnValue($record, $column) ?> - - +```php + + $column): ?> + getColumnValue($record, $column) ?> + + +``` ### Extending column definitions You can extend the columns of another controller from outside by calling the `extendListColumns` static method on the controller class. This method can take two arguments, **$list** will represent the Lists widget object and **$model** represents the model used by the list. Take this controller for example: - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.ListController']; +```php +class Categories extends \Backend\Classes\Controller +{ + public $implement = ['Backend.Behaviors.ListController']; - public $listConfig = 'list_config.yaml'; - } + public $listConfig = 'list_config.yaml'; +} +``` Using the `extendListColumns` method you can add extra columns to any list rendered by this controller. It is a good idea to check the **$model** is of the correct type. Here is an example: - Categories::extendListColumns(function($list, $model) - { - if (!$model instanceof MyModel) { - return; - } +```php +Categories::extendListColumns(function($list, $model) +{ + if (!$model instanceof MyModel) { + return; + } - $list->addColumns([ - 'my_column' => [ - 'label' => 'My Column' - ] - ]); + $list->addColumns([ + 'my_column' => [ + 'label' => 'My Column' + ] + ]); - }); +}); +``` You can also extend the list columns internally by overriding the `listExtendColumns` method inside the controller class. - class Categories extends \Backend\Classes\Controller - { - [...] +```php +class Categories extends \Backend\Classes\Controller +{ + [...] - public function listExtendColumns($list) - { - $list->addColumns([...]); - } + public function listExtendColumns($list) + { + $list->addColumns([...]); } +} +``` The following methods are available on the $list object. Method | Description ------------- | ------------- -**addColumns** | adds new columns to the list -**removeColumn** | removes a column from the list +`addColumns` | adds new columns to the list +`removeColumn` | removes a column from the list Each method takes an array of columns similar to the [list column configuration](#list-columns). @@ -820,138 +916,158 @@ Each method takes an array of columns similar to the [list column configuration] You can inject a custom css row class by adding a `listInjectRowClass` method on the controller class. This method can take two arguments, **$record** will represent a single model record and **$definition** contains the name of the List widget definition. You can return any string value containing your row classes. These classes will be added to the row's HTML markup. - class Lessons extends \Backend\Classes\Controller +```php +class Lessons extends \Backend\Classes\Controller +{ + [...] + + public function listInjectRowClass($lesson, $definition) { - [...] - - public function listInjectRowClass($lesson, $definition) - { - // Strike through past lessons - if ($lesson->lesson_date->lt(Carbon::today())) { - return 'strike'; - } + // Strike through past lessons + if ($lesson->lesson_date->lt(Carbon::today())) { + return 'strike'; } } +} +``` A special CSS class `nolink` is available to force a row to be unclickable, even if the `recordUrl` or `recordOnClick` options are defined for the List widget. Returning this class in an event will allow you to make records unclickable - for example, for soft-deleted rows or for informational rows: - public function listInjectRowClass($record, $value) - { - if ($record->trashed()) { - return 'nolink'; - } - } +```php +public function listInjectRowClass($record, $value) +{ + if ($record->trashed()) { + return 'nolink'; + } +} +``` ### Extending filter scopes You can extend the filter scopes of another controller from outside by calling the `extendListFilterScopes` static method on the controller class. This method can take the argument **$filter** which will represent the Filter widget object. Take this controller for example: - Categories::extendListFilterScopes(function($filter) { - // Add custom CSS classes to the Filter widget itself - $filter->cssClasses = array_merge($filter->cssClasses, ['my', 'array', 'of', 'classes']); - - $filter->addScopes([ - 'my_scope' => [ - 'label' => 'My Filter Scope' - ] - ]); - }); +```php +Categories::extendListFilterScopes(function($filter) { + // Add custom CSS classes to the Filter widget itself + $filter->cssClasses = array_merge($filter->cssClasses, ['my', 'array', 'of', 'classes']); + + $filter->addScopes([ + 'my_scope' => [ + 'label' => 'My Filter Scope' + ] + ]); +}); +``` > The array of scopes provided is similar to the [list filters configuration](#list-filters). You can also extend the filter scopes internally to the controller class, simply override the `listFilterExtendScopes` method. - class Categories extends \Backend\Classes\Controller - { - [...] +```php +class Categories extends \Backend\Classes\Controller +{ + [...] - public function listFilterExtendScopes($filter) - { - $filter->addScopes([...]); - } + public function listFilterExtendScopes($filter) + { + $filter->addScopes([...]); } +} +``` The following methods are available on the $filter object. Method | Description ------------- | ------------- -**addScopes** | adds new scopes to filter widget -**removeScope** | remove scope from filter widget +`addScopes` | adds new scopes to filter widget +`removeScope` | remove scope from filter widget ### Extending the model query The lookup query for the list [database model](../database/model) can be extended by overriding the `listExtendQuery` method inside the controller class. This example will ensure that soft deleted records are included in the list data, by applying the **withTrashed** scope to the query: - public function listExtendQuery($query) - { - $query->withTrashed(); - } +```php +public function listExtendQuery($query) +{ + $query->withTrashed(); +} +``` When dealing with multiple lists definitions in a same controller, you can use the second parameter of `listExtendQuery` which contains the name of the definition : - public $listConfig = [ - 'inbox' => 'config_inbox_list.yaml', - 'trashed' => 'config_trashed_list.yaml' - ]; - - public function listExtendQuery($query, $definition) - { - if ($definition === 'trashed') { - $query->onlyTrashed(); - } +```php +public $listConfig = [ + 'inbox' => 'config_inbox_list.yaml', + 'trashed' => 'config_trashed_list.yaml' +]; + +public function listExtendQuery($query, $definition) +{ + if ($definition === 'trashed') { + $query->onlyTrashed(); } +} +``` The [list filter](#list-filters) model query can also be extended by overriding the `listFilterExtendQuery` method: - public function listFilterExtendQuery($query, $scope) - { - if ($scope->scopeName == 'status') { - $query->where('status', '<>', 'all'); - } +```php +public function listFilterExtendQuery($query, $scope) +{ + if ($scope->scopeName == 'status') { + $query->where('status', '<>', 'all'); } +} +``` ### Extending the records collection The collection of records used by the list can be extended by overriding the `listExtendRecords` method inside the controller class. This example uses the `sort` method on the [record collection](../database/collection) to change the sort order of the records. - public function listExtendRecords($records) - { - return $records->sort(function ($a, $b) { - return $a->computedVal() > $b->computedVal(); - }); - } +```php +public function listExtendRecords($records) +{ + return $records->sort(function ($a, $b) { + return $a->computedVal() > $b->computedVal(); + }); +} +``` ### Custom column types Custom list column types can be registered in the backend with the `registerListColumnTypes` method of the [Plugin registration class](../plugin/registration#registration-methods). The method should return an array where the key is the type name and the value is a callable function. The callable function receives three arguments, the native `$value`, the `$column` definition object and the model `$record` object. - public function registerListColumnTypes() - { - return [ - // A local method, i.e $this->evalUppercaseListColumn() - 'uppercase' => [$this, 'evalUppercaseListColumn'], - - // Using an inline closure - 'loveit' => function($value) { return 'I love '. $value; } - ]; - } +```php +public function registerListColumnTypes() +{ + return [ + // A local method, i.e $this->evalUppercaseListColumn() + 'uppercase' => [$this, 'evalUppercaseListColumn'], + + // Using an inline closure + 'loveit' => function($value) { return 'I love '. $value; } + ]; +} - public function evalUppercaseListColumn($value, $column, $record) - { - return strtoupper($value); - } +public function evalUppercaseListColumn($value, $column, $record) +{ + return strtoupper($value); +} +``` Using the custom list column type is as simple as calling it by name using the `type` option. - # =================================== - # List Column Definitions - # =================================== +```yaml +# =================================== +# List Column Definitions +# =================================== - columns: - secret_code: - label: Secret code - type: uppercase +columns: + secret_code: + label: Secret code + type: uppercase +``` diff --git a/backend-relations.md b/backend-relations.md index 806c9da7..4f6205da 100644 --- a/backend-relations.md +++ b/backend-relations.md @@ -19,18 +19,20 @@ The **Relation behavior** is a controller [behavior](../services/behaviors) used The Relation behavior depends on [relation definitions](#relation-definitions). In order to use the relation behavior you should add the `Backend.Behaviors.RelationController` definition to the `$implement` field of the controller class. Also, the `$relationConfig` class property should be defined and its value should refer to the YAML file used for [configuring the behavior options](#configuring-relation). - namespace Acme\Projects\Controllers; +```php +namespace Acme\Projects\Controllers; - class Projects extends Controller - { - public $implement = [ - 'Backend.Behaviors.FormController', - 'Backend.Behaviors.RelationController', - ]; +class Projects extends Controller +{ + public $implement = [ + 'Backend.Behaviors.FormController', + 'Backend.Behaviors.RelationController', + ]; - public $formConfig = 'config_form.yaml'; - public $relationConfig = 'config_relation.yaml'; - } + public $formConfig = 'config_form.yaml'; + public $relationConfig = 'config_relation.yaml'; +} +``` > **NOTE:** Very often the relation behavior is used together with the [form behavior](forms). @@ -41,85 +43,90 @@ The configuration file referred in the `$relationConfig` property is defined in The first level field in the relation configuration file defines the relationship name in the target model. For example: - class Invoice { - public $hasMany = [ - 'items' => ['Acme\Pay\Models\InvoiceItem'], - ]; - } +```php +class Invoice { + public $hasMany = [ + 'items' => ['Acme\Pay\Models\InvoiceItem'], + ]; +} +``` An *Invoice* model with a relationship called `items` should define the first level field using the same relationship name: - # =================================== - # Relation Behavior Config - # =================================== - - items: - label: Invoice Line Item - view: - list: $/acme/pay/models/invoiceitem/columns.yaml - toolbarButtons: create|delete - manage: - form: $/acme/pay/models/invoiceitem/fields.yaml - recordsPerPage: 10 +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +items: + label: Invoice Line Item + view: + list: $/acme/pay/models/invoiceitem/columns.yaml + toolbarButtons: create|delete + manage: + form: $/acme/pay/models/invoiceitem/fields.yaml + recordsPerPage: 10 +``` You can also customize the labels of the toolbar buttons: - items: - label: Invoice Line Item - view: - list: $/acme/pay/models/invoiceitem/columns.yaml - toolbarButtons: - create: Add a line item - delete: Remove line item - manage: - form: $/acme/pay/models/invoiceitem/fields.yaml - recordsPerPage: 10 - +```yaml +items: + label: Invoice Line Item + view: + list: $/acme/pay/models/invoiceitem/columns.yaml + toolbarButtons: + create: Add a line item + delete: Remove line item + manage: + form: $/acme/pay/models/invoiceitem/fields.yaml + recordsPerPage: 10 +``` The following options are then used for each relationship name definition: Option | Description ------------- | ------------- -**label** | a label for the relation, in the singular tense, required. -**view** | configuration specific to the view container, see below. -**manage** | configuration specific to the management popup, see below. -**pivot** | a reference to form field definition file, used for [relations with pivot table data](#belongs-to-many-pivot). -**emptyMessage** | a message to display when the relationship is empty, optional. -**readOnly** | disables the ability to add, update, delete or create relations. default: false -**deferredBinding** | [defers all binding actions using a session key](../database/model#deferred-binding) when it is available. default: false +`label` | a label for the relation, in the singular tense, required. +`view` | configuration specific to the view container, see below. +`manage` | configuration specific to the management popup, see below. +`pivot` | a reference to form field definition file, used for [relations with pivot table data](#belongs-to-many-pivot). +`emptyMessage` | a message to display when the relationship is empty, optional. +`readOnly` | disables the ability to add, update, delete or create relations. default: `false` +`deferredBinding` | [defers all binding actions using a session key](../database/model#deferred-binding) when it is available. default: `false` These configuration values can be specified for the **view** or **manage** options, where applicable to the render type of list, form or both. Option | Type | Description ------------- | ------------- | ------------- -**form** | Form | a reference to form field definition file, see [backend form fields](forms#form-fields). -**list** | List | a reference to list column definition file, see [backend list columns](lists#list-columns). -**showSearch** | List | display an input for searching the records. Default: false -**showSorting** | List | displays the sorting link on each column. Default: true -**defaultSort** | List | sets a default sorting column and direction when user preference is not defined. Supports a string or an array with keys `column` and `direction`. -**recordsPerPage** | List | maximum rows to display for each page. -**noRecordsMessage** | List | a message to display when no records are found, can refer to a [localization string](../plugin/localization). -**conditions** | List | specifies a raw where query statement to apply to the list model query. -**scope** | List | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. The model that this relationship will be attached to (i.e. the **parent model**) is passed to this scope method as the second parameter (`$query` is the first). +`form` | Form | a reference to form field definition file, see [backend form fields](forms#form-fields). +`list` | List | a reference to list column definition file, see [backend list columns](lists#list-columns). +`showSearch` | List | display an input for searching the records. Default: `false` +`showSorting` | List | displays the sorting link on each column. Default: `true` +`defaultSort` | List | sets a default sorting column and direction when user preference is not defined. Supports a string or an array with keys `column` and `direction`. +`recordsPerPage` | List | maximum rows to display for each page. +`noRecordsMessage` | List | a message to display when no records are found, can refer to a [localization string](../plugin/localization). +`conditions` | List | specifies a raw where query statement to apply to the list model query. +`scope` | List | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. The model that this relationship will be attached to (i.e. the **parent model**) is passed to this scope method as the second parameter (`$query` is the first). **filter** | List | a reference to a filter scopes definition file, see [backend list filters](lists#list-filters). These configuration values can be specified only for the **view** options. Option | Type | Description ------------- | ------------- | ------------- -**showCheckboxes** | List | displays checkboxes next to each record. -**recordUrl** | List | link each list record to another page. Eg: **users/update/:id**. The `:id` part is replaced with the record identifier. -**customViewPath** | List | specify a custom view path to override partials used by the list. -**recordOnClick** | List | custom JavaScript code to execute when clicking on a record. -**toolbarPartial** | Both | a reference to a controller partial file with the toolbar buttons. Eg: **_relation_toolbar.htm**. This option overrides the *toolbarButtons* option. -**toolbarButtons** | Both | the set of buttons to display. This can be formatted as an array or a pipe separated string, or set to `false` to show no buttons. Available options are: `create`, `update`, `delete`, `add`, `remove`, `link`, & `unlink`. Example: `add\|remove`.
Additionally, you can customize the text inside these buttons by setting this property to an associative array, with the key being the button type and the value being the text for that button. Example: `create: 'Assign User'`. The value also supports translation. +`showCheckboxes` | List | displays checkboxes next to each record. +`recordUrl` | List | link each list record to another page. Eg: **users/update/:id**. The `:id` part is replaced with the record identifier. +`customViewPath` | List | specify a custom view path to override partials used by the list. +`recordOnClick` | List | custom JavaScript code to execute when clicking on a record. +`toolbarPartial` | Both | a reference to a controller partial file with the toolbar buttons. Eg: **_relation_toolbar.htm**. This option overrides the *toolbarButtons* option. +`toolbarButtons` | Both | the set of buttons to display. This can be formatted as an array or a pipe separated string, or set to `false` to show no buttons. Available options are: `create`, `update`, `delete`, `add`, `remove`, `link`, & `unlink`. Example: `add\|remove`.
Additionally, you can customize the text inside these buttons by setting this property to an associative array, with the key being the button type and the value being the text for that button. Example: `create: 'Assign User'`. The value also supports translation. These configuration values can be specified only for the **manage** options. Option | Type | Description ------------- | ------------- | ------------- -**title** | Both | a popup title, can refer to a [localization string](../plugin/localization).
Additionally, you can customize the title for each mode individually by setting this to an associative array, with the key being the mode and the value being the title used when displaying that mode. Eg: `form: acme.blog::lang.subcategory.FormTitle`. -**context** | Form | context of the form being displayed. Can be a string or an array with keys: create, update. +`title` | Both | a popup title, can refer to a [localization string](../plugin/localization).
Additionally, you can customize the title for each mode individually by setting this to an associative array, with the key being the mode and the value being the title used when displaying that mode. Eg: `form: acme.blog::lang.subcategory.FormTitle`. +`context` | Form | context of the form being displayed. Can be a string or an array with keys: create, update. ## Relationship types @@ -144,18 +151,20 @@ How the relation manager is displayed depends on the relationship definition in For example, if a *Blog Post* has many *Comments*, the target model is set as the blog post and a list of comments is displayed, using columns from the **list** definition. Clicking on a comment opens a popup form with the fields defined in **form** to update the comment. Comments can be created in the same way. Below is an example of the relation behavior configuration file: - # =================================== - # Relation Behavior Config - # =================================== - - comments: - label: Comment - manage: - form: $/acme/blog/models/comment/fields.yaml - list: $/acme/blog/models/comment/columns.yaml - view: - list: $/acme/blog/models/comment/columns.yaml - toolbarButtons: create|delete +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +comments: + label: Comment + manage: + form: $/acme/blog/models/comment/fields.yaml + list: $/acme/blog/models/comment/columns.yaml + view: + list: $/acme/blog/models/comment/columns.yaml + toolbarButtons: create|delete +``` ### Belongs to many @@ -168,18 +177,20 @@ For example, if a *Blog Post* has many *Comments*, the target model is set as th For example, if a *User* belongs to many *Roles*, the target model is set as the user and a list of roles is displayed, using columns from the **list** definition. Existing roles can be added and removed from the user. Below is an example of the relation behavior configuration file: - # =================================== - # Relation Behavior Config - # =================================== - - roles: - label: Role - view: - list: $/acme/user/models/role/columns.yaml - toolbarButtons: add|remove - manage: - list: $/acme/user/models/role/columns.yaml - form: $/acme/user/models/role/fields.yaml +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +roles: + label: Role + view: + list: $/acme/user/models/role/columns.yaml + toolbarButtons: add|remove + manage: + list: $/acme/user/models/role/columns.yaml + form: $/acme/user/models/role/fields.yaml +``` ### Belongs to many (with Pivot Data) @@ -191,44 +202,48 @@ For example, if a *User* belongs to many *Roles*, the target model is set as the Continuing the example in *Belongs To Many* relations, if a role also carried an expiry date, clicking on a role will open a popup form with the fields defined in **pivot** to update the expiry date. Below is an example of the relation behavior configuration file: - # =================================== - # Relation Behavior Config - # =================================== - - roles: - label: Role - view: - list: $/acme/user/models/role/columns.yaml - manage: - list: $/acme/user/models/role/columns.yaml - pivot: - form: $/acme/user/models/role/fields.yaml +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +roles: + label: Role + view: + list: $/acme/user/models/role/columns.yaml + manage: + list: $/acme/user/models/role/columns.yaml + pivot: + form: $/acme/user/models/role/fields.yaml +``` Pivot data is available when defining form fields and list columns via the `pivot` relation, see the example below: - # =================================== - # Relation Behavior Config - # =================================== - - teams: - label: Team - view: - list: - columns: - name: - label: Name - pivot[team_color]: - label: Team color - manage: - list: - columns: - name: - label: Name - pivot: - form: - fields: - pivot[team_color]: - label: Team color +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +teams: + label: Team + view: + list: + columns: + name: + label: Name + pivot[team_color]: + label: Team color + manage: + list: + columns: + name: + label: Name + pivot: + form: + fields: + pivot[team_color]: + label: Team color +``` ### Belongs to @@ -242,18 +257,20 @@ Pivot data is available when defining form fields and list columns via the `pivo For example, if a *Phone* belongs to a *Person* the relation manager will display a form with the fields defined in **form**. Clicking the Link button will display a list of People to associate with the Phone. Clicking the Unlink button will dissociate the Phone with the Person. - # =================================== - # Relation Behavior Config - # =================================== - - person: - label: Person - view: - form: $/acme/user/models/person/fields.yaml - toolbarButtons: link|unlink - manage: - form: $/acme/user/models/person/fields.yaml - list: $/acme/user/models/person/columns.yaml +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +person: + label: Person + view: + form: $/acme/user/models/person/fields.yaml + toolbarButtons: link|unlink + manage: + form: $/acme/user/models/person/fields.yaml + list: $/acme/user/models/person/columns.yaml +``` ### Has one @@ -267,38 +284,46 @@ For example, if a *Phone* belongs to a *Person* the relation manager will displa For example, if a *Person* has one *Phone* the relation manager will display form with the fields defined in **form** for the Phone. When clicking the Update button, a popup is displayed with the fields now editable. If the Person already has a Phone the fields are update, otherwise a new Phone is created for them. - # =================================== - # Relation Behavior Config - # =================================== - - phone: - label: Phone - view: - form: $/acme/user/models/phone/fields.yaml - toolbarButtons: update|delete - manage: - form: $/acme/user/models/phone/fields.yaml - list: $/acme/user/models/phone/columns.yaml +```yaml +# =================================== +# Relation Behavior Config +# =================================== + +phone: + label: Phone + view: + form: $/acme/user/models/phone/fields.yaml + toolbarButtons: update|delete + manage: + form: $/acme/user/models/phone/fields.yaml + list: $/acme/user/models/phone/columns.yaml +``` ## Displaying a relation manager Before relations can be managed on any page, the target model must first be initialized in the controller by calling the `initRelation` method. - $post = Post::where('id', 7)->first(); - $this->initRelation($post); +```php +$post = Post::where('id', 7)->first(); +$this->initRelation($post); +``` > **NOTE:** The [form behavior](forms) will automatically initialize the model on its create, update and preview actions. The relation manager can then be displayed for a specified relation definition by calling the `relationRender` method. For example, if you want to display the relation manager on the [Preview](forms#form-preview-view) page, the **preview.htm** view contents could look like this: - formRenderPreview() ?> +```php +formRenderPreview() ?> - relationRender('comments') ?> +relationRender('comments') ?> +``` You may instruct the relation manager to render in read only mode by passing the option as the second argument: - relationRender('comments', ['readOnly' => true]) ?> +```php +relationRender('comments', ['readOnly' => true]) ?> +``` ## Extending relation behavior @@ -317,17 +342,19 @@ Sometimes you may wish to modify the default relation behavior and there are sev Provides an opportunity to manipulate the relation configuration. The following example can be used to inject a different columns.yaml file based on a property of your model. - public function relationExtendConfig($config, $field, $model) - { - // Make sure the model and field matches those you want to manipulate - if (!$model instanceof MyModel || $field != 'myField') - return; +```php +public function relationExtendConfig($config, $field, $model) +{ + // Make sure the model and field matches those you want to manipulate + if (!$model instanceof MyModel || $field != 'myField') + return; - // Show a different list for business customers - if ($model->mode == 'b2b') { - $config->view['list'] = '$/author/plugin_name/models/mymodel/b2b_columns.yaml'; - } + // Show a different list for business customers + if ($model->mode == 'b2b') { + $config->view['list'] = '$/author/plugin_name/models/mymodel/b2b_columns.yaml'; } +} +``` ### Extending the view widget @@ -337,78 +364,88 @@ Provides an opportunity to manipulate the view widget. For example you might want to toggle showCheckboxes based on a property of your model. - public function relationExtendViewWidget($widget, $field, $model) - { - // Make sure the model and field matches those you want to manipulate - if (!$model instanceof MyModel || $field != 'myField') - return; +```php +public function relationExtendViewWidget($widget, $field, $model) +{ + // Make sure the model and field matches those you want to manipulate + if (!$model instanceof MyModel || $field != 'myField') + return; - if ($model->constant) { - $widget->showCheckboxes = false; - } + if ($model->constant) { + $widget->showCheckboxes = false; } +} +``` #### How to remove a column Since the widget has not completed initializing at this point of the runtime cycle you can't call $widget->removeColumn(). The addColumns() method as described in the [ListController documentation](/docs/backend/lists#extend-list-columns) will work as expected, but to remove a column we need to listen to the 'list.extendColumns' event within the relationExtendViewWidget() method. The following example shows how to remove a column: - public function relationExtendViewWidget($widget, $field, $model) - { - // Make sure the model and field matches those you want to manipulate - if (!$model instanceof MyModel || $field != 'myField') - return; +```php +public function relationExtendViewWidget($widget, $field, $model) +{ + // Make sure the model and field matches those you want to manipulate + if (!$model instanceof MyModel || $field != 'myField') + return; - // Will not work! - $widget->removeColumn('my_column'); + // Will not work! + $widget->removeColumn('my_column'); - // This will work - $widget->bindEvent('list.extendColumns', function () use($widget) { - $widget->removeColumn('my_column'); - }); - } + // This will work + $widget->bindEvent('list.extendColumns', function () use($widget) { + $widget->removeColumn('my_column'); + }); +} +``` ### Extending the manage widget Provides an opportunity to manipulate the manage widget of your relation. - public function relationExtendManageWidget($widget, $field, $model) - { - // Make sure the field is the expected one - if ($field != 'myField') - return; +```php +public function relationExtendManageWidget($widget, $field, $model) +{ + // Make sure the field is the expected one + if ($field != 'myField') + return; - // manipulate widget as needed - } + // manipulate widget as needed +} +``` ### Extending the pivot widget Provides an opportunity to manipulate the pivot widget of your relation. - public function relationExtendPivotWidget($widget, $field, $model) - { - // Make sure the field is the expected one - if ($field != 'myField') - return; +```php +public function relationExtendPivotWidget($widget, $field, $model) +{ + // Make sure the field is the expected one + if ($field != 'myField') + return; - // manipulate widget as needed - } + // manipulate widget as needed +} +``` ### Extending the filter widgets There are two filter widgets that may be extended using the following methods, one for the view mode and one for the manage mode of the `RelationController`. - public function relationExtendViewFilterWidget($widget, $field, $model) - { - // Extends the view filter widget - } +```php +public function relationExtendViewFilterWidget($widget, $field, $model) +{ + // Extends the view filter widget +} - public function relationExtendManageFilterWidget($widget, $field, $model) - { - // Extends the manage filter widget - } +public function relationExtendManageFilterWidget($widget, $field, $model) +{ + // Extends the manage filter widget +} +``` Examples on how to add or remove scopes programmatically in the filter widgets can be found in the **Extending filter scopes** section of the [backend list documentation](/docs/backend/lists#extend-filter-scopes). @@ -417,14 +454,16 @@ Examples on how to add or remove scopes programmatically in the filter widgets c The view widget is often refreshed when the manage widget makes a change, you can use this method to inject additional containers when this process occurs. Return an array with the extra values to send to the browser, eg: - public function relationExtendRefreshResults($field) - { - // Make sure the field is the expected one - if ($field != 'myField') - return; +```php +public function relationExtendRefreshResults($field) +{ + // Make sure the field is the expected one + if ($field != 'myField') + return; - return ['#myCounter' => 'Total records: 6']; - } + return ['#myCounter' => 'Total records: 6']; +} +``` ## Overriding relation partials diff --git a/backend-reorder.md b/backend-reorder.md index 59a14bf7..d3c02677 100644 --- a/backend-reorder.md +++ b/backend-reorder.md @@ -20,58 +20,63 @@ The behavior depends on a [model class](../database/model) which must implement In order to use the reorder behavior you should add it to the `$implement` property of the controller class. Also, the `$reorderConfig` class property should be defined and its value should refer to the YAML file used for configuring the behavior options. - namespace Acme\Shop\Controllers; +```php +namespace Acme\Shop\Controllers; - class Categories extends Controller - { - public $implement = [ - 'Backend.Behaviors.ReorderController', - ]; +class Categories extends Controller +{ + public $implement = [ + 'Backend.Behaviors.ReorderController', + ]; - public $reorderConfig = 'config_reorder.yaml'; + public $reorderConfig = 'config_reorder.yaml'; - // [...] - } + // [...] +} +``` ## Configuring the behavior The configuration file referred in the `$reorderConfig` property is defined in YAML format. The file should be placed into the controller's [views directory](controllers-ajax/#introduction). Below is an example of a configuration file: - # =================================== - # Reorder Behavior Config - # =================================== +```yaml +# =================================== +# Reorder Behavior Config +# =================================== - # Reorder Title - title: Reorder Categories +# Reorder Title +title: Reorder Categories - # Attribute name - nameFrom: title +# Attribute name +nameFrom: title - # Model Class name - modelClass: Acme\Shop\Models\Category - - # Toolbar widget configuration - toolbar: - # Partial for toolbar buttons - buttons: reorder_toolbar +# Model Class name +modelClass: Acme\Shop\Models\Category +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: reorder_toolbar +``` The configuration options listed below can be used. Option | Description ------------- | ------------- -**title** | used for the page title. -**nameFrom** | specifies which attribute should be used as a label for each record. -**modelClass** | a model class name, the record data is loaded from this model. -**toolbar** | reference to a Toolbar Widget configuration file, or an array with configuration. +`title` | used for the page title. +`nameFrom` | specifies which attribute should be used as a label for each record. +`modelClass` | a model class name, the record data is loaded from this model. +`toolbar` | reference to a Toolbar Widget configuration file, or an array with configuration. ## Displaying the reorder page You should provide a [view file](controllers-ajax/#introduction) with the name **reorder.htm**. This view represents the Reorder page that allows users to reorder records. Since reordering includes the toolbar, the view file will consist solely of the single `reorderRender` method call. - reorderRender() ?> +```php +reorderRender() ?> +``` ## Override Sortable Partials @@ -92,7 +97,9 @@ in The lookup query for the list [database model](../database/model) can be extended by overriding the `reorderExtendQuery` method inside the controller class. This example will ensure that soft deleted records are included in the list data, by applying the **withTrashed** scope to the query: - public function reorderExtendQuery($query) - { - $query->withTrashed(); - } +```php +public function reorderExtendQuery($query) +{ + $query->withTrashed(); +} +``` diff --git a/backend-users.md b/backend-users.md index 0d2d72fa..10d7fc18 100644 --- a/backend-users.md +++ b/backend-users.md @@ -32,94 +32,112 @@ Groups (`\Backend\Models\UserGroup`) are an organizational tool for grouping adm The global `BackendAuth` facade can be used for managing administrative users, which primarily inherits the `Winter\Storm\Auth\Manager` class. To register a new administrator user account, use the `BackendAuth::register` method. - $user = BackendAuth::register([ - 'first_name' => 'Some', - 'last_name' => 'User', - 'login' => 'someuser', - 'email' => 'some@website.tld', - 'password' => 'changeme', - 'password_confirmation' => 'changeme' - ]); +```php +$user = BackendAuth::register([ + 'first_name' => 'Some', + 'last_name' => 'User', + 'login' => 'someuser', + 'email' => 'some@website.tld', + 'password' => 'changeme', + 'password_confirmation' => 'changeme' +]); +``` The `BackendAuth::check` method is a quick way to check if the user is signed in. To return the user model that is signed in, use `BackendAuth::getUser` instead. Additionally, the active user will be available as `$this->user` inside any [backend controller](../backend/controllers-ajax). - // Returns true if signed in. - $loggedIn = BackendAuth::check(); +```php +// Returns true if signed in. +$loggedIn = BackendAuth::check(); - // Returns the signed in user - $user = BackendAuth::getUser(); +// Returns the signed in user +$user = BackendAuth::getUser(); - // Returns the signed in user from a controller - $user = $this->user; +// Returns the signed in user from a controller +$user = $this->user; +``` You may look up a user by their login name using the `BackendAuth::findUserByLogin` method. - $user = BackendAuth::findUserByLogin('someuser'); +```php +$user = BackendAuth::findUserByLogin('someuser'); +``` You may authenticate a user by providing their login and password with `BackendAuth::authenticate`. You can also authenticate as a user simply by passing the `Backend\Models\User` model along with `BackendAuth::login`. - // Authenticate user by credentials - $user = BackendAuth::authenticate([ - 'login' => post('login'), - 'password' => post('password') - ]); +```php +// Authenticate user by credentials +$user = BackendAuth::authenticate([ + 'login' => post('login'), + 'password' => post('password') +]); - // Sign in as a specific user - BackendAuth::login($user); +// Sign in as a specific user +BackendAuth::login($user); +``` ## Registering permissions Plugins can register backend user permissions by overriding the `registerPermissions` method inside the [Plugin registration class](../plugin/registration#registration-file). The permissions are defined as an array with keys corresponding the permission keys and values corresponding the permission descriptions. The permission keys consist of the author name, the plugin name and the feature name. Here is an example code: - acme.blog.access_categories +```none +acme.blog.access_categories +``` The next example shows how to register backend permission items. Permissions are defined with a permission key and description. In the backend permission management user interface permissions are displayed as a checkbox list. Backend controllers can use permissions defined by plugins for restricting the user access to [pages](#page-access) or [features](#features). - public function registerPermissions() - { - return [ - 'acme.blog.access_posts' => [ - 'label' => 'Manage the blog posts', - 'tab' => 'Blog', - 'order' => 200, - ], - // ... - ]; - } +```php +public function registerPermissions() +{ + return [ + 'acme.blog.access_posts' => [ + 'label' => 'Manage the blog posts', + 'tab' => 'Blog', + 'order' => 200, + ], + // ... + ]; +} +``` You may also specify a `roles` option as an array with each value as a role API code. When a role is created with this code, it becomes a system role that always grants this permission to users with that role. - public function registerPermissions() - { - return [ - 'acme.blog.access_categories' => [ - 'label' => 'Manage the blog categories', - 'tab' => 'Blog', - 'order' => 200, - 'roles' => ['developer'] - ] - // ... - ]; - } +```php +public function registerPermissions() +{ + return [ + 'acme.blog.access_categories' => [ + 'label' => 'Manage the blog categories', + 'tab' => 'Blog', + 'order' => 200, + 'roles' => ['developer'] + ] + // ... + ]; +} +``` ## Restricting access to backend pages In a backend controller class you can specify which permissions are required for access the pages provided by the controller. It's done with the `$requiredPermissions` controller's property. This property should contain an array of permission keys. If the user permissions match any permission from the list, the framework will let the user to see the controller pages. - ## Restricting access to features @@ -128,25 +146,29 @@ The backend user model has methods that allow to determine whether the user has The `hasAccess` method returns **true** for any permission if the user is a superuser (`is_superuser` set to `true`). The `hasPermission` method is more strict, only returning true if the user actually has the specified permissions either in their account or through their role. Generally, `hasAccess` is the preferred method to use as it respects the absolute power of the superuser. The following example shows how to use the methods in the controller code: - if ($this->user->hasAccess('acme.blog.*')) { - // ... - } +```php +if ($this->user->hasAccess('acme.blog.*')) { + // ... +} - if ($this->user->hasPermission([ - 'acme.blog.access_posts', - 'acme.blog.access_categories' - ])) { - // ... - } +if ($this->user->hasPermission([ + 'acme.blog.access_posts', + 'acme.blog.access_categories' +])) { + // ... +} +``` You can also use the methods in the backend views for hiding user interface elements. The next examples demonstrates how you can hide a button on the Edit Category [backend form](forms): - user->hasAccess('acme.blog.delete_categories')): ?> - - +```html +user->hasAccess('acme.blog.delete_categories')): ?> + + +``` diff --git a/backend-views-partials.md b/backend-views-partials.md index 8c65cb64..591b9bdc 100644 --- a/backend-views-partials.md +++ b/backend-views-partials.md @@ -11,59 +11,71 @@ Backend partials are files with the extension **htm** that reside in the [controller's views](#introduction) directory. The partial file names should start with the underscore: *_partial.htm*. Partials can be rendered from a backend page or another partial. Use the controller's `makePartial` method to render a partial. The method takes two parameters - the partial name and the optional array of variables to pass to the partial. Example: - makePartial('sidebar', ['showHeader' => true]) ?> +```php +makePartial('sidebar', ['showHeader' => true]) ?> +``` ### Hint partials You can render informative panels in the backend, called hints, that the user can hide. The first parameter should be a unique key for the purposes of remembering if the hint has been hidden or not. The second parameter is a reference to a partial view. The third parameter can be some extra view variables to pass to the partial, in addition to some hint properties. - makeHintPartial('my_hint_key', 'my_hint_partial', ['foo' => 'bar']) ?> +```php +makeHintPartial('my_hint_key', 'my_hint_partial', ['foo' => 'bar']) ?> +``` You can also disable the ability to hide a hint by setting the key value to a null value. This hint will always be displayed: - makeHintPartial(null, 'my_hint_partial') ?> +```php +makeHintPartial(null, 'my_hint_partial') ?> +``` The following properties are available: Property | Description ------------- | ------------- -**type** | Sets the color of the hint, supported types: danger, info, success, warning. Default: info. -**title** | Adds a title section to the hint. -**subtitle** | In addition to the title, adds a second line to the title section. -**icon** | In addition to the title, adds an icon to the title section. +`type` | Sets the color of the hint, supported types: `danger`, `info`, `success`, `warning`. Default: `info`. +`title` | Adds a title section to the hint. +`subtitle` | In addition to the title, adds a second line to the title section. +`icon` | In addition to the title, adds an icon to the title section. ### Checking if hints are hidden If you're using hints, you may find it useful to check if the user has hidden them. This is easily done using the `isBackendHintHidden` method. It takes a single parameter, and that's the unique key you specified in the original call to `makeHintPartial`. The method will return true if the hint was hidden, false otherwise: - isBackendHintHidden('my_hint_key')): ?> - - +```php +isBackendHintHidden('my_hint_key')): ?> + + +``` ## Layouts and child layouts Backend layouts reside in an optional **layouts/** directory of a plugin. A custom layout is set with the `$layout` property of the controller object. It defaults to the system layout called `default`. - /** - * @var string Layout to use for the view. - */ - public $layout = 'mycustomlayout'; +```php +/** + * @var string Layout to use for the view. + */ +public $layout = 'mycustomlayout'; +``` Layouts also provide the option to attach custom CSS classes to the BODY tag. This can be set with the `$bodyClass` property of the controller. - /** - * @var string Body CSS class to add to the layout. - */ - public $bodyClass = 'compact-container'; +```php +/** + * @var string Body CSS class to add to the layout. + */ +public $bodyClass = 'compact-container'; +``` These body classes are available for the default layout: -- **compact-container** - uses no padding on all sides. -- **slim-container** - uses no padding left and right. -- **breadcrumb-flush** - tells the page breadcrumb to sit flush against the element below. +- `compact-container` - uses no padding on all sides. +- `slim-container` - uses no padding left and right. +- `breadcrumb-flush` - tells the page breadcrumb to sit flush against the element below. ### Form with sidebar @@ -72,25 +84,29 @@ Layouts can also be used in the same way as partials, acting more like a global Before using this layout style, ensure that your controller uses the body class `compact-container` by setting it in your controller's action method or constructor. - $this->bodyClass = 'compact-container'; +```php +$this->bodyClass = 'compact-container'; +``` This layout uses two placeholders, a primary content area called **form-contents** and a complimentary sidebar called **form-sidebar**. Here is an example: - - - Main content - - - - - Side content - - - - - 'layout stretch']) ?> - makeLayout('form-with-sidebar') ?> - - +```php + + + Main content + + + + + Side content + + + + + 'layout stretch']) ?> + makeLayout('form-with-sidebar') ?> + + +``` The layout is executed in the final section by overriding the **body** placeholder used by every backend layout. It wraps everything with a `
` HTML tag and renders the child layout called **form-with-sidebar**. This file is located in `modules\backend\layouts\form-with-sidebar.htm`. diff --git a/backend-widgets.md b/backend-widgets.md index 1b1933b9..af52fdb4 100644 --- a/backend-widgets.md +++ b/backend-widgets.md @@ -40,74 +40,90 @@ Widget classes reside inside the **widgets** directory of the plugin directory. The generic widget classes must extend the `Backend\Classes\WidgetBase` class. As any other plugin class, generic widget controllers should belong to the [plugin namespace](../plugin/registration#namespaces). Example widget controller class definition: - makePartial('list'); - } +```php +public function render() +{ + return $this->makePartial('list'); +} +``` To pass variables to partials you can either add them to the `$vars` property. - public function render() - { - $this->vars['var'] = 'value'; +```php +public function render() +{ + $this->vars['var'] = 'value'; - return $this->makePartial('list'); - } + return $this->makePartial('list'); +} +``` Alternatively you may pass the variables to the second parameter of the makePartial() method: - public function render() - { - return $this->makePartial('list', ['var' => 'value']); - } +```php +public function render() +{ + return $this->makePartial('list', ['var' => 'value']); +} +``` ### AJAX handlers Widgets implement the same AJAX approach as the [backend controllers](controllers-ajax#ajax). The AJAX handlers are public methods of the widget class with names starting with the **on** prefix. The only difference between the widget AJAX handlers and backend controller's AJAX handlers is that you should use the widget's `getEventHandler` method to return the widget's handler name when you refer to it in the widget partials. - Next +```html +Next +``` When called from a widget class or partial the AJAX handler will target itself. For example, if the widget uses the alias of **mywidget** the handler will be targeted with `mywidget::onName`. The above would output the following attribute value: - data-request="mywidget::onPaginate" +```none +data-request="mywidget::onPaginate" +``` ### Binding widgets to controllers A widget should be bound to a [backend controller](controllers-ajax) before you can start using it in a backend page or partial. Use the widget's `bindToController` method for binding it to a controller. The best place to initialize a widget is the controller's constructor. Example: - public function __construct() - { - parent::__construct(); +```php +public function __construct() +{ + parent::__construct(); - $myWidget = new MyWidgetClass($this); - $myWidget->alias = 'myWidget'; - $myWidget->bindToController(); - } + $myWidget = new MyWidgetClass($this); + $myWidget->alias = 'myWidget'; + $myWidget->bindToController(); +} +``` After binding the widget you can access it in the controller's view or partial by its alias: - widget->myWidget->render() ?> +```php +widget->myWidget->render() ?> +``` ## Form Widgets @@ -132,93 +148,101 @@ Form Widget classes reside inside the **formwidgets** directory of the plugin di The form widget classes must extend the `Backend\Classes\FormWidgetBase` class. As any other plugin class, generic widget controllers should belong to the [plugin namespace](../plugin/registration#namespaces). A registered widget can be used in the backend [form field definition](../backend/forms#form-fields) file. Example form widget class definition: - namespace Backend\Widgets; +```php +namespace Backend\Widgets; - use Backend\Classes\FormWidgetBase; +use Backend\Classes\FormWidgetBase; - class CodeEditor extends FormWidgetBase - { - /** - * @var string A unique alias to identify this widget. - */ - protected $defaultAlias = 'codeeditor'; +class CodeEditor extends FormWidgetBase +{ + /** + * @var string A unique alias to identify this widget. + */ + protected $defaultAlias = 'codeeditor'; - public function render() {} - } + public function render() {} +} +``` ### Form widget properties Form widgets may have properties that can be set using the [form field configuration](../backend/forms#form-fields). Simply define the configurable properties on the class and then call the `fillFromConfig` method to populate them inside the `init` method definition. - class DatePicker extends FormWidgetBase +```php +class DatePicker extends FormWidgetBase +{ + // + // Configurable properties + // + + /** + * @var bool Display mode: datetime, date, time. + */ + public $mode = 'datetime'; + + /** + * @var string the minimum/earliest date that can be selected. + * eg: 2000-01-01 + */ + public $minDate = null; + + /** + * @var string the maximum/latest date that can be selected. + * eg: 2020-12-31 + */ + public $maxDate = null; + + // + // Object properties + // + + /** + * {@inheritDoc} + */ + protected $defaultAlias = 'datepicker'; + + /** + * {@inheritDoc} + */ + public function init() { - // - // Configurable properties - // - - /** - * @var bool Display mode: datetime, date, time. - */ - public $mode = 'datetime'; - - /** - * @var string the minimum/earliest date that can be selected. - * eg: 2000-01-01 - */ - public $minDate = null; - - /** - * @var string the maximum/latest date that can be selected. - * eg: 2020-12-31 - */ - public $maxDate = null; - - // - // Object properties - // - - /** - * {@inheritDoc} - */ - protected $defaultAlias = 'datepicker'; - - /** - * {@inheritDoc} - */ - public function init() - { - $this->fillFromConfig([ - 'mode', - 'minDate', - 'maxDate', - ]); - } - - // ... + $this->fillFromConfig([ + 'mode', + 'minDate', + 'maxDate', + ]); } + // ... +} +``` + The property values then become available to set from the [form field definition](../backend/forms#form-fields) when using the widget. - born_at: - label: Date of Birth - type: datepicker - mode: date - minDate: 1984-04-12 - maxDate: 2014-04-23 +```yaml +born_at: + label: Date of Birth + type: datepicker + mode: date + minDate: 1984-04-12 + maxDate: 2014-04-23 +``` ### Form widget registration Plugins should register form widgets by overriding the `registerFormWidgets` method inside the [Plugin registration class](../plugin/registration#registration-file). The method returns an array containing the widget class in the keys and widget short code as the value. Example: - public function registerFormWidgets() - { - return [ - 'Backend\FormWidgets\CodeEditor' => 'codeeditor', - 'Backend\FormWidgets\RichEditor' => 'richeditor' - ]; - } +```php +public function registerFormWidgets() +{ + return [ + 'Backend\FormWidgets\CodeEditor' => 'codeeditor', + 'Backend\FormWidgets\RichEditor' => 'richeditor' + ]; +} +``` The short code is optional and can be used when referencing the widget in the [Form field definitions](forms#field-widget), it should be a unique value to avoid conflicts with other form fields. @@ -227,35 +251,43 @@ The short code is optional and can be used when referencing the widget in the [F The main purpose of the form widget is to interact with your model, which means in most cases loading and saving the value via the database. When a form widget renders, it will request its stored value using the `getLoadValue` method. The `getId` and `getFieldName` methods will return a unique identifier and name for a HTML element used in the form. These values are often passed to the widget partial at render time. - public function render() - { - $this->vars['id'] = $this->getId(); - $this->vars['name'] = $this->getFieldName(); - $this->vars['value'] = $this->getLoadValue(); +```php +public function render() +{ + $this->vars['id'] = $this->getId(); + $this->vars['name'] = $this->getFieldName(); + $this->vars['value'] = $this->getLoadValue(); - return $this->makePartial('myformwidget'); - } + return $this->makePartial('myformwidget'); +} +``` At a basic level the form widget can send the user input value back using an input element. From the above example, inside the **myformwidget** partial the element can be rendered using the prepared variables. - +```html + +``` ### Saving form data When the time comes to take the user input and store it in the database, the form widget will call the `getSaveValue` internally to request the value. To modify this behavior simply override the method in your form widget class. - public function getSaveValue($value) - { - return $value; - } +```php +public function getSaveValue($value) +{ + return $value; +} +``` In some cases you intentionally don't want any value to be given, for example, a form widget that displays information without saving anything. Return the special constant called `FormField::NO_SAVE_DATA` derived from the `Backend\Classes\FormField` class to have the value ignored. - public function getSaveValue($value) - { - return \Backend\Classes\FormField::NO_SAVE_DATA; - } +```php +public function getSaveValue($value) +{ + return \Backend\Classes\FormField::NO_SAVE_DATA; +} +``` ## Report Widgets @@ -280,76 +312,82 @@ The report widget classes should extend the `Backend\Classes\ReportWidgetBase` c Example report widget class definition: - namespace Winter\GoogleAnalytics\ReportWidgets; +```php +namespace Winter\GoogleAnalytics\ReportWidgets; - use Backend\Classes\ReportWidgetBase; +use Backend\Classes\ReportWidgetBase; - class TrafficSources extends ReportWidgetBase +class TrafficSources extends ReportWidgetBase +{ + public function render() { - public function render() - { - return $this->makePartial('widget'); - } + return $this->makePartial('widget'); } +} +``` The widget partial could contain any HTML markup you want to display in the widget. The markup should be wrapped into the DIV element with the **report-widget** class. Using H3 element to output the widget header is preferable. Example widget partial: -
-

Traffic sources

- -
-
    -
  • Direct 1000
  • -
  • Social networks 800
  • -
-
+```html +
+

Traffic sources

+ +
+
    +
  • Direct 1000
  • +
  • Social networks 800
  • +
+
+``` ![image](https://raw.githubusercontent.com/wintercms/docs/main/images/traffic-sources.png) Inside report widgets you can use any [charts or indicators](../ui/form), lists or any other markup you wish. Remember that the report widgets extend the generic backend widgets and you can use any widget functionality in your report widgets. The next example shows a list report widget markup. -
-

Top pages

- -
- - - - - - - - - - - - - - - - - - - - -
Page URLPageviews% Pageviews
/90 -
-
- 90% -
-
/docs10 -
-
- 10% -
-
-
+```html +
+

Top pages

+ +
+ + + + + + + + + + + + + + + + + + + + +
Page URLPageviews% Pageviews
/90 +
+
+ 90% +
+
/docs10 +
+
+ 10% +
+
+
+``` ### Report widget properties @@ -360,48 +398,52 @@ Report widgets may have properties that users can manage with the Inspector: The properties should be defined in the `defineProperties` method of the widget class. The properties are described in the [components article](../plugin/components#component-properties). Example: - public function defineProperties() - { - return [ - 'title' => [ - 'title' => 'Widget title', - 'default' => 'Top Pages', - 'type' => 'string', - 'validationPattern' => '^.+$', - 'validationMessage' => 'The Widget Title is required.' - ], - 'days' => [ - 'title' => 'Number of days to display data for', - 'default' => '7', - 'type' => 'string', - 'validationPattern' => '^[0-9]+$' - ] - ]; - } +```php +public function defineProperties() +{ + return [ + 'title' => [ + 'title' => 'Widget title', + 'default' => 'Top Pages', + 'type' => 'string', + 'validationPattern' => '^.+$', + 'validationMessage' => 'The Widget Title is required.' + ], + 'days' => [ + 'title' => 'Number of days to display data for', + 'default' => '7', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$' + ] + ]; +} +``` ### Report widget registration Plugins can register report widgets by overriding the `registerReportWidgets` method inside the [Plugin registration class](../plugin/registration#registration-file). The method should return an array containing the widget classes in the keys and widget configuration (label, context, and required permissions) in the values. Example: - public function registerReportWidgets() - { - return [ - 'Winter\GoogleAnalytics\ReportWidgets\TrafficOverview' => [ - 'label' => 'Google Analytics traffic overview', - 'context' => 'dashboard', - 'permissions' => [ - 'winter.googleanalytics.widgets.traffic_overview', - ], +```php +public function registerReportWidgets() +{ + return [ + 'Winter\GoogleAnalytics\ReportWidgets\TrafficOverview' => [ + 'label' => 'Google Analytics traffic overview', + 'context' => 'dashboard', + 'permissions' => [ + 'winter.googleanalytics.widgets.traffic_overview', ], - 'Winter\GoogleAnalytics\ReportWidgets\TrafficSources' => [ - 'label' => 'Google Analytics traffic sources', - 'context' => 'dashboard', - 'permissions' => [ - 'winter.googleanaltyics.widgets.traffic_sources', - ], - ] - ]; - } + ], + 'Winter\GoogleAnalytics\ReportWidgets\TrafficSources' => [ + 'label' => 'Google Analytics traffic sources', + 'context' => 'dashboard', + 'permissions' => [ + 'winter.googleanaltyics.widgets.traffic_sources', + ], + ] + ]; +} +``` -The **label** element defines the widget name for the Add Widget popup window. The **context** element defines the context where the widget could be used. Winter's report widget system allows to host the report container on any page, and the container context name is unique. The widget container on the Dashboard page uses the **dashboard** context. +The `label` element defines the widget name for the Add Widget popup window. The `context` element defines the context where the widget could be used. Winter's report widget system allows to host the report container on any page, and the container context name is unique. The widget container on the Dashboard page uses the `dashboard` context. From 31371298bfd13c81c7a407793b7f67ca1d95589d Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Mon, 27 Dec 2021 19:53:17 +0200 Subject: [PATCH 02/26] Improving usability - ajax section --- ajax-attributes-api.md | 48 +++++++---- ajax-extras.md | 182 +++++++++++++++++++++++----------------- ajax-handlers.md | 108 ++++++++++++++---------- ajax-introduction.md | 42 ++++++---- ajax-javascript-api.md | 148 ++++++++++++++++++++------------ ajax-update-partials.md | 87 +++++++++++-------- 6 files changed, 370 insertions(+), 245 deletions(-) diff --git a/ajax-attributes-api.md b/ajax-attributes-api.md index a27dc949..30e646d9 100644 --- a/ajax-attributes-api.md +++ b/ajax-attributes-api.md @@ -21,7 +21,7 @@ Attribute | Description `data-request-redirect` | specifies a URL to redirect the browser after the successful AJAX request. `data-request-url` | specifies a URL to which the request is sent. default: `window.location.href` `data-request-update` | specifies a list of partials and page elements (CSS selectors) to update. The format is as follows: `partial: selector, partial: selector`. Usage of quotes is required in some cases, for example: `'my-partial': '#myelement'`. If the selector string is prepended with the `@` symbol, the content received from the server will be appended to the element, instead of replacing the existing content. If the selector string is prepended with the `^` symbol, the content will be prepended instead. -`data-request-ajax-global` | false by default. Set true to enable jQuery [ajax events](http://api.jquery.com/category/ajax/global-ajax-event-handlers/) globally : `ajaxStart`, `ajaxStop`, `ajaxComplete`, `ajaxError`, `ajaxSuccess` and `ajaxSend`. +`data-request-ajax-global` | `false` by default. Set `true` to enable jQuery [ajax events](https://api.jquery.com/category/ajax/global-ajax-event-handlers/) globally : `ajaxStart`, `ajaxStop`, `ajaxComplete`, `ajaxError`, `ajaxSuccess` and `ajaxSend`. `data-request-data` | specifies additional POST parameters to be sent to the server. The format is following: `var: value, var: value`. Use quotes if needed: `var: 'some string'`. The attribute can be used on the triggering element, for example on the button that also has the `data-request` attribute, on the closest element of the triggering element and on the parent form element. The framework merges values of the `data-request-data` attributes. If the attribute on different elements defines parameters with the same name, the framework uses the following priority: the triggering element `data-request-data`, the closer parent elements `data-request-data`, the form input data. `data-request-before-update` | specifies JavaScript code to execute directly before the page contents are updated. Inside the JavaScript code you can access the following variables: `this` (the page element triggered the request), the `context` object, the `data` object received from the server, the `textStatus` text string, and the `jqXHR` object. `data-request-success` | specifies JavaScript code to execute after the request is successfully completed. Inside the JavaScript code you can access the following variables: `this` (the page element triggered the request), the `context` object, the `data` object received from the server, the `textStatus` text string, and the `jqXHR` object. @@ -46,38 +46,52 @@ Element | Event ## Usage examples -Trigger the `onCalculate` handler when the form is submitted. Update the element with the identifier "result"` with the **calcresult** partial: +Trigger the `onCalculate` handler when the form is submitted. Update the element with the identifier "result" with the **calcresult** partial: - +```html + +``` Request a confirmation when the Delete button is clicked before the request is sent: - - ... - +```html + + ... + +``` Redirect to another page after the successful request: - +```html + +``` Show a popup window after the successful request: - +```html + +``` Send a POST parameter `mode` with a value `update`: - +```html + +``` Send a POST parameter `id` with value `7` across multiple elements: -
- - -
+```html +
+ + +
+``` Including [file uploads](../services/request-input#files) with a request: - - - - +```html +
+ + +
+``` diff --git a/ajax-extras.md b/ajax-extras.md index e17b11ea..a933358b 100644 --- a/ajax-extras.md +++ b/ajax-extras.md @@ -27,21 +27,25 @@ When an AJAX request starts the `ajaxPromise` event is fired that displays the i You may specify the `data-request-validate` attribute on a form to enable validation features. -
- -
+```html +
+ +
+``` ### Throwing a validation error In the server side AJAX handler you may throw a [validation exception](../services/error-log#validation-exception) using the `ValidationException` class to make a field invalid, where the first argument is an array. The array should use field names for the keys and the error messages for the values. - function onSubmit() - { - throw new ValidationException(['name' => 'You must give a name!']); - } +```php +function onSubmit() +{ + throw new ValidationException(['name' => 'You must give a name!']); +} +``` > **NOTE**: You can also pass an instance of the [validation service](../services/validation) as the first argument of the exception. @@ -50,76 +54,92 @@ In the server side AJAX handler you may throw a [validation exception](../servic Inside the form, you may display the first error message by using the `data-validate-error` attribute on a container element. The content inside the container will be set to the error message and the element will be made visible. -
+```html +
+``` To display multiple error messages, include an element with the `data-message` attribute. In this example the paragraph tag will be duplicated and set with content for each message that exists. -
-

-
+```html +
+

+
+``` To add custom classes on AJAX invalidation, hook into the `ajaxInvalidField` and `ajaxPromise` JS events. - $(window).on('ajaxInvalidField', function(event, fieldElement, fieldName, errorMsg, isFirst) { - $(fieldElement).closest('.form-group').addClass('has-error'); - }); +```js +$(window).on('ajaxInvalidField', function(event, fieldElement, fieldName, errorMsg, isFirst) { + $(fieldElement).closest('.form-group').addClass('has-error'); +}); - $(document).on('ajaxPromise', '[data-request]', function() { - $(this).closest('form').find('.form-group.has-error').removeClass('has-error'); - }); +$(document).on('ajaxPromise', '[data-request]', function() { + $(this).closest('form').find('.form-group.has-error').removeClass('has-error'); +}); +``` ### Displaying errors with fields Alternatively, you can show validation messages for individual fields by defining an element that uses the `data-validate-for` attribute, passing the field name as the value. - - +```html + + - -
+ +
+``` If the element is left empty, it will be populated with the validation text from the server. Otherwise you can specify any text you like and it will be displayed instead. -
- Oops.. phone number is invalid! -
+```html +
+ Oops.. phone number is invalid! +
+``` ## Loading button When any element contains the `data-attach-loading` attribute, the CSS class `wn-loading` will be added to it during the AJAX request. This class will spawn a *loading spinner* on button and anchor elements using the `:after` CSS selector. -
- -
- - - Do something - +```html +
+ +
+ + + Do something + +``` ## Flash messages Specify the `data-request-flash` attribute on a form to enable the use of flash messages on successful AJAX requests. -
- -
+```html +
+ +
+``` Combined with use of the `Flash` facade in the event handler, a flash message will appear after the request finishes. - function onSuccess() - { - Flash::success('You did it!'); - } +```php +function onSuccess() +{ + Flash::success('You did it!'); +} +``` To remain consistent with AJAX based flash messages, you can render a [standard flash message](../markup/tag-flash) when the page loads by placing this code in your page or layout. @@ -139,47 +159,51 @@ To remain consistent with AJAX based flash messages, you can render a [standard Below is a complete example of form validation. It calls the `onDoSomething` event handler that triggers a loading submit button, performs validation on the form fields, then displays a successful flash message. -
+```html + -
- - -
+
+ + +
-
- - -
+
+ + +
- + -
-

-
+
+

+
-
+ +``` The AJAX event handler looks at the POST data sent by the client and applies some rules to the validator. If the validation fails, a `ValidationException` is thrown, otherwise a `Flash::success` message is returned. - function onDoSomething() - { - $data = post(); +```php +function onDoSomething() +{ + $data = post(); - $rules = [ - 'name' => 'required', - 'email' => 'required|email', - ]; + $rules = [ + 'name' => 'required', + 'email' => 'required|email', + ]; - $validation = Validator::make($data, $rules); + $validation = Validator::make($data, $rules); - if ($validation->fails()) { - throw new ValidationException($validation); - } - - Flash::success('Jobs done!'); + if ($validation->fails()) { + throw new ValidationException($validation); } + + Flash::success('Jobs done!'); +} +``` diff --git a/ajax-handlers.md b/ajax-handlers.md index 7feb17c8..38986ea2 100644 --- a/ajax-handlers.md +++ b/ajax-handlers.md @@ -12,10 +12,12 @@ AJAX event handlers are PHP functions that can be defined in the page or layout [PHP section](../cms/themes#php-section) or inside [components](../cms/components). Handler names should have the following pattern: `onName`. All handlers support the use of [updating partials](../ajax/update-partials) as part of the AJAX request. - function onSubmitContactForm() - { - // ... - } +```php +function onSubmitContactForm() +{ + // ... +} +``` If two handlers with the same name are defined in a page and layout together, the page handler will be executed. The handlers defined in [components](../cms/components) have the lowest priority. @@ -24,73 +26,91 @@ If two handlers with the same name are defined in a page and layout together, th Every AJAX request should specify a handler name, either using the [data attributes API](../ajax/attributes-api) or the [JavaScript API](../ajax/javascript-api). When the request is made, the server will search all the registered handlers and locate the first one it finds. - - +```html + + - - + + +``` If two components register the same handler name, it is advised to prefix the handler with the [component short name or alias](../cms/components#aliases). If a component uses an alias of **mycomponent** the handler can be targeted with `mycomponent::onName`. - +```html + +``` You may want to use the [`__SELF__`](../plugin/components#referencing-self) reference variable instead of the hard coded alias in case the user changes the component alias used on the page. -
+```twig + +``` #### Generic handler Sometimes you may need to make an AJAX request for the sole purpose of updating page contents, not needing to execute any code. You may use the `onAjax` handler for this purpose. This handler is available everywhere without needing to write any code. - +```html + +``` ## Redirects in AJAX handlers If you need to redirect the browser to another location, return the `Redirect` object from the AJAX handler. The framework will redirect the browser as soon as the response is returned from the server. Example AJAX handler: - function onRedirectMe() - { - return Redirect::to('http://google.com'); - } +```php +function onRedirectMe() +{ + return Redirect::to('http://google.com'); +} +``` ## Returning data from AJAX handlers In advanced cases you may want to return structured data from your AJAX handlers. If an AJAX handler returns an array, you can access its elements in the `success` event handler. Example AJAX handler: - function onFetchDataFromServer() - { - /* Some server-side code */ +```php +function onFetchDataFromServer() +{ + /* Some server-side code */ - return [ - 'totalUsers' => 1000, - 'totalProjects' => 937 - ]; - } + return [ + 'totalUsers' => 1000, + 'totalProjects' => 937 + ]; +} +``` The data can be fetched with the data attributes API: - +```html + +``` The same with the JavaScript API: - +```html + +``` ## Throwing an AJAX exception You may throw an [AJAX exception](../services/error-log#ajax-exception) using the `AjaxException` class to treat the response as an error while retaining the ability to send response contents as normal. Simply pass the response contents as the first argument of the exception. - throw new AjaxException([ - 'error' => 'Not enough questions', - 'questionsNeeded' => 2 - ]); +```php +throw new AjaxException([ + 'error' => 'Not enough questions', + 'questionsNeeded' => 2 +]); +``` > **NOTE**: When throwing this exception type [partials will be updated](../ajax/update-partials) as normal. @@ -99,14 +119,18 @@ You may throw an [AJAX exception](../services/error-log#ajax-exception) using th Sometimes you may want code to execute before a handler executes. Defining an `onInit` function as part of the [page execution life cycle](../cms/layouts#dynamic-pages) allows code to run before every AJAX handler. - function onInit() - { - // From a page or layout PHP code section - } +```php +function onInit() +{ + // From a page or layout PHP code section +} +``` You may define an `init` method inside a [component class](../plugin/components#page-cycle-init) or [backend widget class](../backend/widgets). - function init() - { - // From a component or widget class - } +```php +function init() +{ + // From a component or widget class +} +``` diff --git a/ajax-introduction.md b/ajax-introduction.md index 50bc3407..da2faa0e 100644 --- a/ajax-introduction.md +++ b/ajax-introduction.md @@ -17,7 +17,7 @@ The AJAX framework comes in two flavors, you may either use [the JavaScript API] The AJAX framework is optional in your [CMS theme](../cms/themes), to use the library you should include it by placing the `{% framework %}` tag anywhere inside your [page](../cms/pages) or [layout](../cms/layouts). This adds a reference to the Winter frontend JavaScript library. The library requires jQuery so it should be loaded first, for example: -``` +```twig {% framework %} @@ -46,33 +46,39 @@ A page can issue an AJAX request either prompted by data attributes or by using ## Usage example -Below is a simple example that uses the data attributes API to define an AJAX enabled form. The form will issue an AJAX request to the **onTest** handler and requests that the result container be updated with the **mypartial** partial markup. +Below is a simple example that uses the data attributes API to define an AJAX enabled form. The form will issue an AJAX request to the `onTest` handler and requests that the result container be updated with the `mypartial` partial markup. - - +```html + + - - + + + + - - + + -
+ - -
+ +
+``` > **NOTE**: The form data for `value1` and `value2` are automatically sent with the AJAX request. -The **mypartial** partial contains markup that reads the `result` variable. +The `mypartial` partial contains markup that reads the `result` variable. - The result is {{ result }} +```twig +The result is {{ result }} +``` The **onTest** handler method accessed the form data using the `input` [helper method](../services/helpers#method-input) and the result is passed to the `result` page variable. - function onTest() - { - $this->page['result'] = input('value1') + input('value2'); - } +```php +function onTest() +{ + $this->page['result'] = input('value1') + input('value2'); +} +``` -The example could be read like this: "When the form is submitted, issue an AJAX request to the **onTest** handler. When the handler finishes, render the **mypartial** partial and inject its contents to the **#myDiv** element." +The example could be read like this: "When the form is submitted, issue an AJAX request to the `onTest` handler. When the handler finishes, render the `mypartial` partial and inject its contents to the `#myDiv` element." diff --git a/ajax-javascript-api.md b/ajax-javascript-api.md index a8e60072..4ddb9b4a 100644 --- a/ajax-javascript-api.md +++ b/ajax-javascript-api.md @@ -12,11 +12,19 @@ The JavaScript API is more powerful than the data attributes API. The `request` The `request` method has a single required argument - the AJAX handler name. Example: -
- ... +```html + + ... +``` The second argument of the `request` method is the options object. You can use any option and method compatible with the [jQuery AJAX function](http://api.jquery.com/jQuery.ajax/). The following options are specific for the Winter framework: + +
+ Option | Description ------------- | ------------- `update` | an object, specifies a list partials and page elements (as CSS selectors) to update: {'partial': '#select'}. If the selector string is prepended with the `@` symbol, the content received from the server will be appended to the element, instead of replacing the existing content. @@ -35,6 +43,12 @@ Option | Description You may also override some of the request logic by passing new functions as options. These logic handlers are available. + +
+ Handler | Description ------------- | ------------- `handleConfirmMessage(message)` | called when requesting confirmation from the user. @@ -48,56 +62,76 @@ Handler | Description Request a confirmation before the onDelete request is sent: - $('form').request('onDelete', { - confirm: 'Are you sure?', - redirect: '/dashboard' - }) +```js +$('form').request('onDelete', { + confirm: 'Are you sure?', + redirect: '/dashboard' +}) +``` Run `onCalculate` handler and inject the rendered **calcresult** partial into the page element with the **result** CSS class: - $('form').request('onCalculate', { - update: {calcresult: '.result'} - }) +```js +$('form').request('onCalculate', { + update: {calcresult: '.result'} +}) +``` Run `onCalculate` handler with some extra data: - $('form').request('onCalculate', {data: {value: 55}}) +```js +$('form').request('onCalculate', {data: {value: 55}}) +``` Run `onCalculate` handler and run some custom code before the page elements update: - $('form').request('onCalculate', { - update: {calcresult: '.result'}, - beforeUpdate: function() { /* do something */ } - }) +```js +$('form').request('onCalculate', { + update: {calcresult: '.result'}, + beforeUpdate: function() { /* do something */ } +}) +``` Run `onCalculate` handler and if successful, run some custom code and the default `success` function: - $('form').request('onCalculate', {success: function(data) { - //... do something ... - this.success(data); - }}) +```js +$('form').request('onCalculate', {success: function(data) { + //... do something ... + this.success(data); +}}) +``` Execute a request without a FORM element: - $.request('onCalculate', { - success: function() { - console.log('Finished!'); - } - }) +```js +$.request('onCalculate', { + success: function() { + console.log('Finished!'); + } +}) +``` Run `onCalculate` handler and if successful, run some custom code after the default `success` function is done: - $('form').request('onCalculate', {success: function(data) { - this.success(data).done(function() { - //... do something after parent success() is finished ... - }); - }}) +```js +$('form').request('onCalculate', {success: function(data) { + this.success(data).done(function() { + //... do something after parent success() is finished ... + }); +}}) +``` ## Global AJAX events The AJAX framework triggers several events on the updated elements, triggering element, form, and the window object. The events are triggered regardless on which API was used - the data attributes API or the JavaScript API. + +
+ Event | Description ------------- | ------------- `ajaxBeforeSend` | triggered on the window object before sending the request. @@ -124,35 +158,41 @@ Event | Description Executes JavaScript code when the `ajaxUpdate` event is triggered on an element. - $('.calcresult').on('ajaxUpdate', function() { - console.log('Updated!'); - }) +```js +$('.calcresult').on('ajaxUpdate', function() { + console.log('Updated!'); +}) +``` Execute a single request that shows a Flash Message using logic handler. - $.request('onDoSomething', { - flash: 1, - handleFlashMessage: function(message, type) { - $.wn.flashMsg({ text: message, class: type }) - } - }) +```js +$.request('onDoSomething', { + flash: 1, + handleFlashMessage: function(message, type) { + $.wn.flashMsg({ text: message, class: type }) + } +}) +``` Applies configurations to all AJAX requests globally. - $(document).on('ajaxSetup', function(event, context) { - // Enable AJAX handling of Flash messages on all AJAX requests - context.options.flash = true - - // Enable the StripeLoadIndicator on all AJAX requests - context.options.loading = $.wn.stripeLoadIndicator - - // Handle Error Messages by triggering a flashMsg of type error - context.options.handleErrorMessage = function(message) { - $.wn.flashMsg({ text: message, class: 'error' }) - } - - // Handle Flash Messages by triggering a flashMsg of the message type - context.options.handleFlashMessage = function(message, type) { - $.wn.flashMsg({ text: message, class: type }) - } - }) +```js +$(document).on('ajaxSetup', function(event, context) { + // Enable AJAX handling of Flash messages on all AJAX requests + context.options.flash = true + + // Enable the StripeLoadIndicator on all AJAX requests + context.options.loading = $.wn.stripeLoadIndicator + + // Handle Error Messages by triggering a flashMsg of type error + context.options.handleErrorMessage = function(message) { + $.wn.flashMsg({ text: message, class: 'error' }) + } + + // Handle Flash Messages by triggering a flashMsg of the message type + context.options.handleFlashMessage = function(message, type) { + $.wn.flashMsg({ text: message, class: type }) + } +}) +``` diff --git a/ajax-update-partials.md b/ajax-update-partials.md index 83c3afc2..b4855264 100644 --- a/ajax-update-partials.md +++ b/ajax-update-partials.md @@ -19,19 +19,23 @@ The client browser may request partials to be updated from the server when it pe The [data attributes API](../ajax/attributes-api) uses the `data-request-update` attribute. - - +```html + + +``` The [JavaScript API](../ajax/javascript-api) uses the `update` configuration option: - - $.request('onRefreshTime', { - update: { mytime: '#myDiv' } - }) +```js +// JavaScript API +$.request('onRefreshTime', { + update: { mytime: '#myDiv' } +}) +``` ### Update definition @@ -43,35 +47,42 @@ The definition of what should be updated is specified as a JSON-like object wher The following will request to update the `#myDiv` element with **mypartial** contents. - mypartial: '#myDiv' +```none +mypartial: '#myDiv' +``` Multiple partials are separated by commas. - firstpartial: '#myDiv', secondpartial: '#otherDiv' +```none +firstpartial: '#myDiv', secondpartial: '#otherDiv' +``` If the partial name contains a slash or a dash, it is important to 'quote' the left side. - 'folder/mypartial': '#myDiv', 'my-partial': '#myDiv' +```none +'folder/mypartial': '#myDiv', 'my-partial': '#myDiv' +``` The target element will always be on the right side since it can also be a HTML element in JavaScript. - mypartial: document.getElementById('myDiv') - +```none +mypartial: document.getElementById('myDiv') +``` ### Appending and prepending content If the selector string is prepended with the `@` symbol, the content received from the server will be appended to the element, instead of replacing the existing content. - - 'folder/append': '@#myDiv' - +```none +'folder/append': '@#myDiv' +``` If the selector string is prepended with the `^` symbol, the content will be prepended instead. - - 'folder/append': '^#myDiv' - +```none +'folder/append': '^#myDiv' +``` ## Pushing partial updates @@ -80,12 +91,14 @@ Comparatively, [AJAX handlers](../ajax/handlers) can *push content updates* to t The following example will update an element on the page with the id **myDiv** using the contents found inside the partial **mypartial**. The `onRefreshTime` handler calls the `renderPartial` method to render the partial contents in PHP. - function onRefreshTime() - { - return [ - '#myDiv' => $this->renderPartial('mypartial') - ]; - } +```php +function onRefreshTime() +{ + return [ + '#myDiv' => $this->renderPartial('mypartial') + ]; +} +``` > **NOTE:** The key name must start with an identifier `#` or class `.` character to trigger a content update. @@ -100,16 +113,20 @@ Depending on the execution context, an [AJAX event handler](../ajax/handlers) ma These examples will provide the **result** variable to a partial for each context: - // From page or layout PHP code section - $this['result'] = 'Hello world!'; +```php +// From page or layout PHP code section +$this['result'] = 'Hello world!'; - // From a component class - $this->page['result'] = 'Hello world!'; +// From a component class +$this->page['result'] = 'Hello world!'; - // From a backend controller or widget - $this->vars['result'] = 'Hello world!'; +// From a backend controller or widget +$this->vars['result'] = 'Hello world!'; +``` This value can then be accessed using Twig in the partial: - - {{ result }} +```twig + +{{ result }} +``` From 2049510fa74740b810055a059847642a57ed100c Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Mon, 27 Dec 2021 20:46:14 +0200 Subject: [PATCH 03/26] Improving usability - cms section --- cms-components.md | 215 +++++++++++++++++++++++--------------- cms-content.md | 31 +++--- cms-layouts.md | 78 ++++++++------ cms-mediamanager.md | 245 ++++++++++++++++++++++++-------------------- cms-pages.md | 186 ++++++++++++++++++++------------- cms-partials.md | 59 ++++++----- cms-themes.md | 114 ++++++++++++--------- 7 files changed, 540 insertions(+), 388 deletions(-) diff --git a/cms-components.md b/cms-components.md index 0ed5e83c..ad58d263 100644 --- a/cms-components.md +++ b/cms-components.md @@ -23,19 +23,23 @@ This article describes the components basics and doesn't explain how to use [com If you use the backend user interface you can add components to your pages, partials and layouts by clicking the component in the Components panel. If you use a text editor you can attach a component to a page or layout by adding its name to the [Configuration](themes#configuration-section) section of the template file. The next example demonstrates how to add a demo To-do component to a page: - title = "Components demonstration" - url = "/components" +```ini +title = "Components demonstration" +url = "/components" - [demoTodo] - maxItems = 20 - == - ... +[demoTodo] +maxItems = 20 +== +... +``` This initializes the component with the properties that are defined in the component section. Many components have properties, but it is not a requirement. Some properties are required, and some properties have default values. If you are not sure what properties are supported by a component, refer to the documentation provided by the developer, or use the Inspector in the Winter backend. The Inspector opens when you click a component in the page or layout component panel. When you refer a component, it automatically creates a page variable that matches the component name (`demoTodo` in the previous example). Components that provide HTML markup can be rendered on a page with the `{% component %}` tag, like this: - {% component 'demoTodo' %} +```twig +{% component 'demoTodo' %} +``` > **NOTE:** If two components with the same name are assigned to a page and layout together, the page component overrides any properties of the layout component. @@ -44,59 +48,76 @@ When you refer a component, it automatically creates a page variable that matche If there are two plugins that register components with the same name, you can attach a component by using its fully qualified class name and assigning it an *alias*: - [Winter\Demo\Components\Todo demoTodoAlias] - maxItems = 20 +```ini +[Winter\Demo\Components\Todo demoTodoAlias] +maxItems = 20 +``` The first parameter in the section is the class name, the second is the component alias name that will be used when attached to the page. If you specified a component alias you should use it everywhere in the page code when you refer to the component. Note that the next example refers to the component alias: - {% component 'demoTodoAlias' %} +```twig +{% component 'demoTodoAlias' %} +``` The aliases also allow you to define multiple components of the same class on a same page by using the short name first and an alias second. This lets you to use multiple instances of a same component on a page. - [demoTodo todoA] - maxItems = 10 - [demoTodo todoB] - maxItems = 20 +```ini +[demoTodo todoA] +maxItems = 10 +[demoTodo todoB] +maxItems = 20 +``` ## Using external property values By default property values are initialized in the Configuration section where the component is defined, and the property values are static, like this: - [demoTodo] - maxItems = 20 - == - ... +```ini +[demoTodo] +maxItems = 20 +== +... +``` However there is a way to initialize properties with values loaded from external parameters - URL parameters or [partial](partials) parameters (for components defined in partials). Use the `{{ paramName }}` syntax for values that should be loaded from partial variables: - [demoTodo] - maxItems = {{ maxItems }} - == - ... +```ini +[demoTodo] +maxItems = {{ maxItems }} +== +... +``` Assuming that in the example above the component **demoTodo** is defined in a partial, it will be initialized with a value loaded from the **maxItems** partial variable: - {% partial 'my-todo-partial' maxItems='10' %} +```twig +{% partial 'my-todo-partial' maxItems='10' %} +``` You may use dot notation to retrieve a deeply nested value from an external parameter: - [demoTodo] - maxItems = {{ data.maxItems }} - == - ... +```ini +[demoTodo] +maxItems = {{ data.maxItems }} +== +... +``` To load a property value from the URL parameter, use the `{{ :paramName }}` syntax, where the name starts with a colon (`:`), for example: - [demoTodo] - maxItems = {{ :maxItems }} - == - ... +```ini +[demoTodo] +maxItems = {{ :maxItems }} +== +... +``` The page, the component belongs to, should have a corresponding [URL parameter](pages#url-syntax) defined: - url = "/todo/:maxItems" - +```ini +url = "/todo/:maxItems" +``` In the Winter backend you can use the Inspector tool for assigning external values to component properties. In the Inspector you don't need to use the curly brackets to enter the parameter name. Each field in the Inspector has an icon on the right side, which opens the external parameter name editor. Enter the parameter name as `paramName` for partial variables or `:paramName` for URL parameters. @@ -106,7 +127,9 @@ Components can be designed to use variables at the time they are rendered, simil In this example, the **maxItems** property of the component will be set to *7* at the time the component is rendered: - {% component 'demoTodoAlias' maxItems='7' %} +```twig +{% component 'demoTodoAlias' maxItems='7' %} +``` > **NOTE**: Not all components support passing variables when rendering. @@ -120,25 +143,33 @@ The markup provided by components is generally intended as a usage example for t Each component can have an entry point partial called **default.htm** that is rendered when the `{% component %}` tag is called, in the following example we will assume the component is called **blogPost**. - url = "blog/post" +```twig +url = "blog/post" - [blogPost] - == - {% component "blogPost" %} +[blogPost] +== +{% component "blogPost" %} +``` The output will be rendered from the plugin directory **components/blogpost/default.htm**. You can copy all the markup from this file and paste it directly in the page or to a new partial, called **blog-post.htm** for example. -

{{ __SELF__.post.title }}

-

{{ __SELF__.post.description }}

+```twig +

{{ __SELF__.post.title }}

+

{{ __SELF__.post.description }}

+``` Inside the markup you may notice references to a variable called `__SELF__`, this refers to the component object and should be replaced with the component alias used on the page, in this example it is `blogPost`. -

{{ blogPost.post.title }}

-

{{ blogPost.post.description }}

+```twig +

{{ blogPost.post.title }}

+

{{ blogPost.post.description }}

+``` This is the only change needed to allow the default component markup to work anywhere inside the theme. Now the component markup can be customized and rendered using the theme partial. - {% partial 'blog-post.htm' %} +```twig +{% partial 'blog-post.htm' %} +``` This process can be repeated for all other partials found in the component partial directory. @@ -147,11 +178,13 @@ This process can be repeated for all other partials found in the component parti All component partials can be overridden using the theme partials. If a component called **channel** uses the **title.htm** partial. - url = "mypage" +```twig +url = "mypage" - [channel] - == - {% component "channel" %} +[channel] +== +{% component "channel" %} +``` We can override the partial by creating a file in our theme called **partials/channel/title.htm**. @@ -159,15 +192,17 @@ The file path segments are broken down like this: Segment | Description ------------- | ------------- -**partials** | the theme partials directory -**channel** | the component alias (a partial subdirectory) -**title.htm** | the component partial to override +`partials` | the theme partials directory +`channel` | the component alias (a partial subdirectory) +`title.htm` | the component partial to override The partial subdirectory name can be customized to anything by simply assigning the component an alias of the same name. For example, by assigning the **channel** component with a different alias **foobar** the override directory is also changed: - [channel foobar] - == - {% component "foobar" %} +```twig +[channel foobar] +== +{% component "foobar" %} +``` Now we can override the **title.htm** partial by creating a file in our theme called **partials/foobar/title.htm**. @@ -176,27 +211,31 @@ Now we can override the **title.htm** partial by creating a file in our theme ca There is a special component included in Winter called `viewBag` that can be used on any page or layout. It allows ad hoc properties to be defined and accessed inside the markup area easily as variables. A good usage example is defining an active menu item inside a page: - title = "About" - url = "/about.html" - layout = "default" +```ini +title = "About" +url = "/about.html" +layout = "default" - [viewBag] - activeMenu = "about" - == +[viewBag] +activeMenu = "about" +== -

Page content...

+

Page content...

+``` Any property defined for the component is then made available inside the page, layout, or partial markup using the `viewBag` variable. For example, in this layout the **active** class is added to the list item if the `viewBag.activeMenu` value is set to **about**: - description = "Default layout" - == - [...] +```twig +description = "Default layout" +== +[...] - -
    -
  • About
  • - [...] -
+ +
    +
  • About
  • + [...] +
+``` > **NOTE**: The viewBag component is hidden on the backend and is only available for file-based editing. It can also be used by other plugins to store data. @@ -209,30 +248,36 @@ When soft components are present on a page and the component is unavailable, no You can define soft components by prefixing the component name with an `@` symbol. - url = "mypage" +```twig +url = "mypage" - [@channel] - == - {% component "channel" %} +[@channel] +== +{% component "channel" %} +``` In this example, should the `channel` component not be available, the `{% component "channel" %}` tag will be ignored when the page is rendered. Soft components also work with aliases as well: - url = "mypage" +```twig +url = "mypage" - [@channel channelSection] - == - {% component "channelSection" %} +[@channel channelSection] +== +{% component "channelSection" %} +``` As soft components do not contain any of the data that the component may provide normally if the component is not available, you must take care to ensure that any custom markup will gracefully handle any missing component information. For example: - url = "mypage" - - [@channel] - == - {% if channel.name %} -
- {% channel.name %} -
- {% endif %} \ No newline at end of file +```twig +url = "mypage" + +[@channel] +== +{% if channel.name %} +
+ {% channel.name %} +
+{% endif %} +``` diff --git a/cms-content.md b/cms-content.md index c8ad7c4b..9f767058 100644 --- a/cms-content.md +++ b/cms-content.md @@ -14,9 +14,9 @@ Content blocks files reside in the **/content** subdirectory of a theme director Extension | Description ------------- | ------------- -**htm** | Used for HTML markup. -**txt** | Used for plain text. -**md** | Used for Markdown syntax. +`htm` | Used for HTML markup. +`txt` | Used for plain text. +`md` | Used for Markdown syntax. The extension affects the way content blocks are displayed in the backend user interface (with a WYSIWYG editor or with a plain text editor) and how the blocks are rendered on the website. Markdown blocks are converted to HTML before they are displayed. @@ -25,22 +25,27 @@ The extension affects the way content blocks are displayed in the backend user i Use the `{% content 'file.htm' %}` tag to render a content block in a [page](pages), [partial](partials) or [layout](layouts). Example of a page rendering a content block: - url = "/contacts" - == -
- {% content 'contacts.htm' %} -
+```twig +url = "/contacts" +== +
+ {% content 'contacts.htm' %} +
+``` ## Passing variables to content blocks Sometimes you may need to pass variables to a content block from the external code. While content blocks do not support the use of Twig markup, they do support using variables with a basic syntax. You can pass variables to content blocks by specifying them after the content block name in the `{% content %}` tag: - {% content 'welcome.htm' name='John' %} - +```twig +{% content 'welcome.htm' name='John' %} +``` Inside the content block, variables can be accessed using singular *curly brackets*: -

This is a demo for {name}

+```twig +

This is a demo for {name}

+``` More information can be found [in the Markup guide](../markup/tag-content). @@ -49,6 +54,8 @@ More information can be found [in the Markup guide](../markup/tag-content). You may register variables that are globally available to all content blocks with the `View::share` method. - View::share('site_name', 'Winter CMS'); +```php +View::share('site_name', 'Winter CMS'); +``` This code could be called inside the register or boot method of a [plugin registration file](../plugin/registration). Using the above example, the variable `{site_name}` will be available inside all content blocks. diff --git a/cms-layouts.md b/cms-layouts.md index 5e10e45b..8a749616 100644 --- a/cms-layouts.md +++ b/cms-layouts.md @@ -12,60 +12,72 @@ Layouts define the page scaffold, usually including everything that is present o Layout templates reside in the **/layouts** subdirectory of a theme directory. Layout template files should have the **htm** extension. Inside the layout file you should use the `{% page %}` tag to output the page content. Simplest layout example: - - - {% page %} - - +```twig + + + {% page %} + + +``` To use a layout for a [page](pages) the page should refer the layout file name (without extension) in the [Configuration](themes#configuration-section) section. Remember that if you refer a layout from a [subdirectory](themes#subdirectories) you should specify the subdirectory name. Example page template using the default.htm layout: - url = "/" - layout = "default" - == -

Hello, world!

+```ini +url = "/" +layout = "default" +== +

Hello, world!

+``` When this page is requested its content is merged with the layout, or more precisely - the layout's `{% page %}` tag is replaced with the page content. The previous examples would generate the following markup: - - -

Hello, world!

- - +```html + + +

Hello, world!

+ + +``` Note that you can render [partials](partials) in layouts. This lets you to share the common markup elements between different layouts. For example, you can have a partial that outputs the website CSS and JavaScript links. This approach simplifies the resource management - if you want to add a JavaScript reference you should modify a single partial instead of editing all the layouts. The [Configuration](themes#configuration-section) section is optional for layouts. The supported configuration parameters are **name** and **description**. The parameters are optional and used in the backend user interface. Example layout template with a description: - description = "Basic layout example" - == - - - {% page %} - - +```twig +description = "Basic layout example" +== + + + {% page %} + + +``` ## Placeholders Placeholders allow pages to inject content to the layout. Placeholders are defined in the layout templates with the `{% placeholder %}` tag. The next example shows a layout template with a placeholder **head** defined in the HTML HEAD section. - - - {% placeholder head %} - - ... +```twig + + + {% placeholder head %} + + ... +``` Pages can inject content to placeholders with the `{% put %}` and `{% endput %}` tags. The following example demonstrates a simple page template which injects a CSS link to the placeholder **head** defined in the previous example. - url = "/my-page" - layout = "default" - == - {% put head %} - - {% endput %} +```twig +url = "/my-page" +layout = "default" +== +{% put head %} + +{% endput %} -

The page content goes here.

+

The page content goes here.

+``` More information on placeholders can be found [in the Markup guide](../markup/tag-placeholder). diff --git a/cms-mediamanager.md b/cms-mediamanager.md index 22a66740..c5568f3a 100644 --- a/cms-mediamanager.md +++ b/cms-mediamanager.md @@ -24,25 +24,29 @@ Create **media** folder in the bucket. The folder name doesn't matter. This fold By default files in S3 buckets cannot be accessed directly. To make the bucket public, return to the bucket list and click the bucket. Click **Properties** button in the right sidebar. Expand **Permissions** tab. Click **Edit bucket policy** link. Paste the following code to the policy popup window. Replace the bucket name with your actual bucket name: - { - "Version": "2008-10-17", - "Id": "Policy1397632521960", - "Statement": [ - { - "Sid": "Stmt1397633323327", - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::BUCKETNAME/*" - } - ] - } +```json +{ + "Version": "2008-10-17", + "Id": "Policy1397632521960", + "Statement": [ + { + "Sid": "Stmt1397633323327", + "Effect": "Allow", + "Principal": { + "AWS": "*" + }, + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::BUCKETNAME/*" + } + ] +} +``` Click **Save** button to apply the policy. The policy gives public read-only access to all folders and directories in the bucket. If you're going to use the bucket for other needs, it's possible to setup a public access to a specific folder in the bucket, just specify the directory name in the **Resource** value: - "arn:aws:s3:::BUCKETNAME/media/*" +```none +"arn:aws:s3:::BUCKETNAME/media/*" +``` You should also create an API user that Winter CMS will use for managing the bucket files. In AWS console go to IAM section. Go to Users tab and create a new user. The user name doesn't matter. Make sure that "Generate an access key for each user" checkbox is checked when you create a new user. After AWS creates a user, it allows you to see the security credentials - the user **Access Key ID** and **Secret Access Key**. Copy the keys and put them into a temporary text file. @@ -52,10 +56,10 @@ Now you have all the information to update Winter CMS configuration. Open **conf Parameter | Value ------------- | ------------- -**key** | the **Access Key ID** value of the user that you created before. -**secret** | the **Secret Access Key** value of the user that you created fore. -**bucket** | your bucket name. -**region** | the bucket region code, see below. +`key` | the **Access Key ID** value of the user that you created before. +`secret` | the **Secret Access Key** value of the user that you created fore. +`bucket` | your bucket name. +`region` | the bucket region code, see below. You can find the bucket region in S3 management console, in the bucket properties. The Properties tab displays the region name, for example Oregon. S3 driver configuration requires a bucket code. Use this table to find code for your bucket (you can also take a look at [AWS documentation](http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region)): @@ -85,40 +89,44 @@ Region | Code Example configuration after update: - 'disks' => [ - ... - 's3' => [ - 'driver' => 's3', - 'key' => 'XXXXXXXXXXXXXXXXXXXX', - 'secret' => 'xxxXxXX+XxxxxXXxXxxxxxxXxxXXXXXXXxxxX9Xx', - 'region' => 'us-west-2', - 'bucket' => 'my-bucket' - ], - ... - ] +```php +'disks' => [ + ... + 's3' => [ + 'driver' => 's3', + 'key' => 'XXXXXXXXXXXXXXXXXXXX', + 'secret' => 'xxxXxXX+XxxxxXXxXxxxxxxXxxXXXXXXXxxxX9Xx', + 'region' => 'us-west-2', + 'bucket' => 'my-bucket' + ], + ... +] +``` Save **config/filesystem.php** script and open **config/cms.php** script. Find the section **storage**. In the **media** parameter update **disk**, **folder** and **path** parameters: Parameter | Value ------------- | ------------- -**disk** | use **s3** value. -**folder** | the name of the folder you created in S3 bucket. -**path** | the public path of the folder in the bucket, see below. +`disk` | use **s3** value. +`folder` | the name of the folder you created in S3 bucket. +`path` | the public path of the folder in the bucket, see below. To obtain the path of the folder, open AWS console and go to S3 section. Navigate to the bucket and click the folder you created before. Upload any file to the folder and click the file. Click **Properties** button in the right sidebar. The file URL is in the **Link** parameter. Copy the URL and remove the file name and the trailing slash from it. Example storage configuration: - 'storage' => [ - ... - 'media' => [ - 'disk' => 's3', - 'folder' => 'media', - 'path' => 'https://s3-us-west-2.amazonaws.com/your-bucket-name/media' - ] +```php +'storage' => [ + ... + 'media' => [ + 'disk' => 's3', + 'folder' => 'media', + 'path' => 'https://s3-us-west-2.amazonaws.com/your-bucket-name/media' ] +] +``` -Congratulations! Now you're ready to use Amazon S3 with Winter CMS. Note that you can also configure Amazon CloudFront CDN to work with your bucket. This topic is not covered in this document, please refer to [CloudFront documentation](http://aws.amazon.com/cloudfront/). After you configure CloudFront, you will need to update the **path** parameter in the storage configuration. +Congratulations! Now you're ready to use Amazon S3 with Winter CMS. Note that you can also configure Amazon CloudFront CDN to work with your bucket. This topic is not covered in this document, please refer to [CloudFront documentation](https://aws.amazon.com/cloudfront/). After you configure CloudFront, you will need to update the **path** parameter in the storage configuration. ## Configuring Rackspace CDN access @@ -135,48 +143,52 @@ Now you have all the information to update Winter CMS configuration. Open **conf Parameter | Value ------------- | ------------- -**username** | Rackspace user name (for example winter.cdn.api). -**key** | the user's **API Key** that you can copy from Rackspace user profile page. -**container** | the container name. -**region** | the bucket region code, see below. -**endpoint** | leave the value as is. -**region** | you can find the region in the CDN container list in Rackspace control panel. The code is a 3-letter value, for example it's **ORD** for Chicago. +`username` | Rackspace user name (for example winter.cdn.api). +`key` | the user's **API Key** that you can copy from Rackspace user profile page. +`container` | the container name. +`region` | the bucket region code, see below. +`endpoint` | leave the value as is. +`region` | you can find the region in the CDN container list in Rackspace control panel. The code is a 3-letter value, for example it's **ORD** for Chicago. Example configuration after update: - 'disks' => [ - ... - 'rackspace' => [ - 'driver' => 'rackspace', - 'username' => 'winter.api.cdn', - 'key' => 'xx00000000xxxxxx0x0x0x000xx0x0x0', - 'container' => 'my-bucket', - 'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/', - 'region' => 'ORD' - ], - ... - ] +```php +'disks' => [ + ... + 'rackspace' => [ + 'driver' => 'rackspace', + 'username' => 'winter.api.cdn', + 'key' => 'xx00000000xxxxxx0x0x0x000xx0x0x0', + 'container' => 'my-bucket', + 'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/', + 'region' => 'ORD' + ], + ... +] +``` Save **config/filesystem.php** script and open **config/cms.php** script. Find the section **storage**. In the **media** parameter update **disk**, **folder** and **path** parameters: Parameter | Value ------------- | ------------- -**disk** | use **rackspace** value. -**folder** | the name of the folder you created in CDN container. -**path** | the public path of the folder in the container, see below. +`disk` | use **rackspace** value. +`folder` | the name of the folder you created in CDN container. +`path` | the public path of the folder in the container, see below. To obtain the path of the folder, go to the CDN container list in Rackspace console. Click the container and open the media folder. Upload any file. After the file is uploaded, click it. The file will open in a new browser tab. Copy the file URL and remove the file name and trailing slash from it. Example storage configuration: - 'storage' => [ - ... - 'media' => [ - 'disk' => 'rackspace', - 'folder' => 'media', - 'path' => 'https://xxxxxxxxx-xxxxxxxxx.r00.cf0.rackcdn.com/media' - ] +```php +'storage' => [ + ... + 'media' => [ + 'disk' => 'rackspace', + 'folder' => 'media', + 'path' => 'https://xxxxxxxxx-xxxxxxxxx.r00.cf0.rackcdn.com/media' ] +] +``` Congratulations! Now you're ready to use Rackspace CDN with Winter CMS. @@ -185,51 +197,60 @@ Congratulations! Now you're ready to use Rackspace CDN with Winter CMS. By default the system uses HTML5 audio and video tags to render audio and video files: - +```html + +``` or - +```html + +``` This behavior can be overridden. If there are **wn-audio-player.htm** and **wn-video-player.htm** CMS partials, they will be used for displaying audio and video contents. Inside the partials use the variable **src** to output a link to the source file. Example: - +```twig + +``` If you don't want to use HTML5 player you can provide any other markup in the partials. There's a [third-party script](https://html5media.info/) that enables support of HTML5 video and audio tags in older browsers. As the partials are written with Twig, you can automate adding alternative video sources based on a naming convention. For example, if there's a convention that there's always a smaller resolution video for each full resolution video, and the smaller resolution file has extension "iphone.mp4", the generated markup could look like this: - +```twig + +``` ## Other configuration options There are several options that allow you to fine-tune the Media Manager. All of them could be defined in **config/cms.php** script, in the **storage/media** section, for example: - 'storage' => [ - ... - - 'media' => [ - ... - 'ignore' => ['.svn', '.git', '.DS_Store'] - ] - ], +```php +'storage' => [ + ... + 'media' => [ + ... + 'ignore' => ['.svn', '.git', '.DS_Store'] + ] +], +``` Parameter | Value ------------- | ------------- -**ignore** | a list of file and directory names to ignore. Defaults to ['.svn', '.git', '.DS_Store']. -**ttl** | specifies the cache time-to-live, in minutes. The default value is 10. The cache invalidates automatically when Library items are added, updated or deleted. -**imageExtensions** | file extensions corresponding to the Image document type. The default value is **['gif', 'png', 'jpg', 'jpeg', 'bmp']**. -**videoExtensions** | file extensions corresponding to the Video document type. The default value is **['mp4', 'avi', 'mov', 'mpg']**. -**audioExtensions** | file extensions corresponding to the Audio document type. The default value is **['mp3', 'wav', 'wma', 'm4a']**. +`ignore` | a list of file and directory names to ignore. Defaults to ['.svn', '.git', '.DS_Store']. +`ttl` | specifies the cache time-to-live, in minutes. The default value is 10. The cache invalidates automatically when Library items are added, updated or deleted. +`imageExtensions` | file extensions corresponding to the Image document type. The default value is `['gif', 'png', 'jpg', 'jpeg', 'bmp']`. +`videoExtensions` | file extensions corresponding to the Video document type. The default value is `['mp4', 'avi', 'mov', 'mpg']`. +`audioExtensions` | file extensions corresponding to the Audio document type. The default value is `['mp3', 'wav', 'wma', 'm4a']`. ## Events @@ -238,28 +259,32 @@ The Media Manager provides a few [events](../services/events) that you can liste Event | Description | Parameters ------------- | ------------- | ------------- -**folder.delete** | Called when a folder is deleted | `(string) $path` -**file.delete** | Called when a file is deleted | `(string) $path` -**folder.rename** | Called when a folder is renamed | `(string) $originalPath`, `(string) $newPath` -**file.rename** | Called when a file is renamed | `(string) $originalPath`, `(string) $newPath` -**folder.create** | Called when a folder is created | `(string) $newFolderPath` -**folder.move** | Called when a folder is moved | `(string) $path`, `(string) $dest` -**file.move** | Called when a file is moved | `(string) $path`, `(string) $dest` -**file.upload** | Called when a file is uploaded | `(string) $filePath`, `(\Symfony\Component\HttpFoundation\File\UploadedFile) $uploadedFile` +`folder.delete` | Called when a folder is deleted | `(string) $path` +`file.delete` | Called when a file is deleted | `(string) $path` +`folder.rename` | Called when a folder is renamed | `(string) $originalPath`, `(string) $newPath` +`file.rename` | Called when a file is renamed | `(string) $originalPath`, `(string) $newPath` +`folder.create` | Called when a folder is created | `(string) $newFolderPath` +`folder.move` | Called when a folder is moved | `(string) $path`, `(string) $dest` +`file.move` | Called when a file is moved | `(string) $path`, `(string) $dest` +`file.upload` | Called when a file is uploaded | `(string) $filePath`, `(\Symfony\Component\HttpFoundation\File\UploadedFile) $uploadedFile` **To hook into these events, either extend the `Backend\Widgets\MediaManager` class directly:** - Backend\Widgets\MediaManager::extend(function($widget) { - $widget->bindEvent('file.rename', function ($originalPath, $newPath) { - // Update custom references to path here - }); +```php +Backend\Widgets\MediaManager::extend(function($widget) { + $widget->bindEvent('file.rename', function ($originalPath, $newPath) { + // Update custom references to path here }); - +}); +``` + **Or listen globally via the `Event` facade (each event is prefixed with `media.` and will be passed the instantiated `Backend\Widgets\MediaManager` object as the first parameter):** - Event::listen('media.file.rename', function($widget, $originalPath, $newPath) { - // Update custom references to path here - }); +```php +Event::listen('media.file.rename', function($widget, $originalPath, $newPath) { + // Update custom references to path here +}); +``` ## Troubleshooting diff --git a/cms-pages.md b/cms-pages.md index ee938a2f..d8f42088 100644 --- a/cms-pages.md +++ b/cms-pages.md @@ -17,9 +17,11 @@ All websites have pages. In Winter, frontend pages are rendered by page templates. Page template files reside in the **/pages** subdirectory of a theme directory. Page file names do not affect the routing, but it's a good idea to name your pages according to the page's function. The files should have the **htm** extension. The [Configuration](themes#configuration-section) and [Twig](themes#twig-section) template sections are required for pages, but the [PHP section](themes#php-section) is optional. Below, you can see the simplest home page example: - url = "/" - == -

Hello, world!

+```ini +url = "/" +== +

Hello, world!

+``` ## Page configuration @@ -28,62 +30,80 @@ Page configuration is defined in the [Configuration Section](themes#configuratio Parameter | Description ------------- | ------------- -**url** | the page URL, required. The URL syntax is described below. -**title** | the page title, required. -**layout** | the page [layout](layouts), optional. If specified, should contain the name of the layout file, without extension, for example: `default`. -**description** | the page description for the backend interface, optional. -**hidden** | hidden pages are accessible only by logged-in backend users, optional. +`url` | the page URL, required. The URL syntax is described below. +`title` | the page title, required. +`layout` | the page [layout](layouts), optional. If specified, should contain the name of the layout file, without extension, for example: `default`. +`description` | the page description for the backend interface, optional. +`hidden` | hidden pages are accessible only by logged-in backend users, optional. ### URL syntax The page URL is defined with the **url** configuration parameter. URLs should start with the forward slash character, and can contain parameters. URLs without parameters are fixed and strict. In the following example, the page URL is `/blog`. - url = "/blog" +```ini +url = "/blog" +``` > **NOTE:** The page URL is case-insensitive by default. URLs with parameters are more flexible. A page with the URL pattern defined in the following example would be displayed for any address like `/blog/post/something`. URL parameters can be accessed by Winter components or from the page [PHP code](themes#php-section) section. - url = "/blog/post/:post_id" +```ini +url = "/blog/post/:post_id" +``` This is how you can access the URL parameter from the page's PHP section (see the [Dynamic pages](#dynamic-pages) section for more details): - url = "/blog/post/:post_id" - == - function onStart() - { - $post_id = $this->param('post_id'); - } - == +```php +url = "/blog/post/:post_id" +== +function onStart() +{ + $post_id = $this->param('post_id'); +} +== +``` Parameter names should be compatible with PHP variable names. To make a parameter optional, add a question mark after its name: - url = "/blog/post/:post_id?" +```ini +url = "/blog/post/:post_id?" +``` Parameters in the middle of the URL cannot be optional. In the next example, the `:post_id` parameter is marked as optional, but is processed as required. - url = "/blog/:post_id?/comments" +```ini +url = "/blog/:post_id?/comments" +``` Optional parameters can have default values which are used as fallback values in case the real parameter value is not presented in the URL. Default values cannot contain any asterisks, pipe symbols, or question marks. The default value is specified after the **question mark**. In the next example, the `category_id` parameter would be `10` for the URL `/blog/category`. - url = "/blog/category/:category_id?10" +```ini +url = "/blog/category/:category_id?10" +``` You can also use regular expressions to validate parameters. To add a validation expression, add a pipe symbol after the parameter name, or a question mark, and specify the expression. The forward slash symbol is not allowed in these expressions. Examples: - url = "/blog/:post_id|^[0-9]+$/comments" - this will match /blog/10/comments - ... - url = "/blog/:post_id|^[0-9]+$" - this will match /blog/3 - ... - url = "/blog/:post_name?|^[a-z0-9\-]+$" - this will match /blog/my-blog-post +```ini +url = "/blog/:post_id|^[0-9]+$/comments" - this will match /blog/10/comments +... +url = "/blog/:post_id|^[0-9]+$" - this will match /blog/3 +... +url = "/blog/:post_name?|^[a-z0-9\-]+$" - this will match /blog/my-blog-post +``` It is possible to use a special *wildcard* parameter by placing an **asterisk** after the parameter. Unlike regular parameters, wildcard parameters can match one or more URL segments. A URL can only ever contain a single wildcard parameter, cannot use regular expressions, or be followed by an optional parameter. - url = "/blog/:category*/:slug" +```ini +url = "/blog/:category*/:slug" +``` Wildcard parameters themselves can be made optional by preceding the asterisk with the `?` character however. - url = "/blog/:slug?*" +```ini +url = "/blog/:slug?*" +``` For example, a URL like `/color/:color/make/:make*/edit` will match `/color/brown/make/volkswagen/beetle/retro/edit` and extract the following parameter values: @@ -104,33 +124,37 @@ Inside the [Twig section](themes#twig-section) of a page template, you can use a There are special functions that can be defined in the PHP section of pages and layouts: `onInit`, `onStart`, and `onEnd`. The `onInit` function is executed when all components are initialized and before AJAX requests are handled. The `onStart` function is executed during the beginning of the page execution. The `onEnd` function is executed before the page is rendered and after the page [components](../cms/components) are executed. In the `onStart` and `onEnd` functions, you can inject variables into the Twig environment. Use `array notation` to pass variables to the page: - url = "/" - == - function onStart() - { - $this['hello'] = "Hello world!"; - } - == -

{{ hello }}

+```php +url = "/" +== +function onStart() +{ + $this['hello'] = "Hello world!"; +} +== +

{{ hello }}

+``` The next example is more complicated. It shows how to load a blog post collection from the database, and display on the page (the Acme\Blog plugin is imaginary): - url = "/blog" - == - use Acme\Blog\Classes\Post; - - function onStart() - { - $this['posts'] = Post::orderBy('created_at', 'desc')->get(); - } - == -

Latest posts

-
    - {% for post in posts %} -

    {{ post.title }}

    - {{ post.content }} - {% endfor %} -
+```twig +url = "/blog" +== +use Acme\Blog\Classes\Post; + +function onStart() +{ + $this['posts'] = Post::orderBy('created_at', 'desc')->get(); +} +== +

Latest posts

+
    + {% for post in posts %} +

    {{ post.title }}

    + {{ post.content }} + {% endfor %} +
+``` The default variables and Twig extensions provided by Winter are described in the [Markup Guide](../markup). The sequence that the handlers are executed in is described by the [Dynamic layouts](layouts#dynamic-layouts) article. @@ -139,35 +163,43 @@ The default variables and Twig extensions provided by Winter are described in th All methods defined in the execution life cycle have the ability to halt the process and return a response - simply return a response from the life cycle function. The example below will not load any page contents, and instead return the string *Hello world!* to the browser: - function onStart() - { - return 'Hello world!'; - } +```php +function onStart() +{ + return 'Hello world!'; +} +``` A more useful example might be triggering a redirect using the `Redirect` facade: - public function onStart() - { - return Redirect::to('http://google.com'); - } +```php +public function onStart() +{ + return Redirect::to('http://google.com'); +} +``` ### Handling forms You can handle standard forms with handler methods defined in the page or layout [PHP section](themes#php-section) (handling the AJAX requests is explained in the [AJAX Framework](../ajax/introduction) article). Use the [`form_open()`](../markup#standard-form) function to define a form that refers to an event handler. Example: - {{ form_open({ request: 'onHandleForm' }) }} - Please enter a string: - - {{ form_close() }} -

Last submitted value: {{ lastValue }}

+```twig +{{ form_open({ request: 'onHandleForm' }) }} + Please enter a string: + +{{ form_close() }} +

Last submitted value: {{ lastValue }}

+``` The `onHandleForm` function can be defined in the page or layout [PHP section](themes#php-section), like so: - function onHandleForm() - { - $this['lastValue'] = post('value'); - } +```php +function onHandleForm() +{ + $this['lastValue'] = post('value'); +} +``` The handler loads the value with the `post` function and initializes the page's `lastValue` attribute variable which is displayed below the form in the first example. @@ -175,7 +207,9 @@ The handler loads the value with the `post` function and initializes the page's If you want to refer to a handler defined in a specific [component](../cms/components), use the component's name or alias in the handler reference: - {{ form_open({ request: 'myComponent::onHandleForm' }) }} +```twig +{{ form_open({ request: 'myComponent::onHandleForm' }) }} +``` ## 404 page @@ -192,14 +226,18 @@ By default, any errors will be shown with a detailed error page containing the f The properties of a page can be accessed in the [PHP code section](../cms/themes#php-section), or [Components](../cms/components) by referencing `$this->page`. - function onEnd() - { - $this->page->title = 'A different page title'; - } +```php +function onEnd() +{ + $this->page->title = 'A different page title'; +} +``` They can also be accessed in the markup using the [`this.page` variable](../markup/this-page). For example, to return the title of a page: -

The title of this page is: {{ this.page.title }}

+```twig +

The title of this page is: {{ this.page.title }}

+``` More information can be found at [`this.page` in the Markup guide](../markup/this-page). diff --git a/cms-partials.md b/cms-partials.md index e645afc4..a5bff5bc 100644 --- a/cms-partials.md +++ b/cms-partials.md @@ -12,15 +12,19 @@ Partials contain reusable chunks of Twig markup that can be used anywhere throughout the website. Partials are extremely useful for page elements that repeat on different pages or layouts. A good partial example is a page footer which is used in different [page layouts](layouts). Also, partials are required for [updating the page content with AJAX](../ajax/update-partials). -Partial templates files reside in the **/partials** subdirectory of a theme directory. Partial files should have the **htm** extension. Example of a simplest possible partial: +Partial templates files reside in the `/partials` subdirectory of a theme directory. Partial files should have the `htm` extension. Example of a simplest possible partial: -

This is a partial

+```html +

This is a partial

+``` The [Configuration](themes#configuration-section) section is optional for partials and can contain the optional **description** parameter which is displayed in the backend user interface. The next example shows a partial with description: - description = "Demo partial" - == -

This is a partial

+```html +description = "Demo partial" +== +

This is a partial

+``` The partial configuration section can also contain component definitions. [Components](components) are explained in another article. @@ -29,29 +33,36 @@ The partial configuration section can also contain component definitions. [Compo The `{% partial "partial-name" %}` Twig tag renders a partial. The tag has a single required parameter - the partial file name without the extension. Remember that if you refer a partial from a [subdirectory](themes#subdirectories) you should specify the subdirectory name. The `{% partial %}` tag can be used inside a page, layout or another partial. Example of a page referring to a partial: - +```twig + +``` ## Passing variables to partials You will find that you often need to pass variables to a partial from the external code. This makes partials even more useful. For example, you can have a partial that renders a list of blog posts. If you can pass the post collection to the partial, the same partial could be used on the blog archive page, on the blog category page and so on. You can pass variables to partials by specifying them after the partial name in the `{% partial %}` tag: - +```twig + +``` You can also assign new variables for use in the partial: - +```twig + +``` Inside the partial, variables can be accessed like any other markup variable: -

Country: {{ country }}, city: {{ city }}.

- +```twig +

Country: {{ country }}, city: {{ city }}.

+``` ## Dynamic partials @@ -63,13 +74,15 @@ Partials, like pages, can use any Twig features. Please refer to the [Dynamic pa There are special functions that can be defined in the PHP section of partials: `onStart` and `onEnd`. The `onStart` function is executed before the partial is rendered and before the partial [components](components) are executed. The `onEnd` function is executed before the partial is rendered and after the partial [components](components) are executed. In the onStart and onEnd functions you can inject variables to the Twig environment. Use the `array notation` to pass variables to the page: - == - function onStart() - { - $this['hello'] = "Hello world!"; - } - == -

{{ hello }}

+```php +== +function onStart() +{ + $this['hello'] = "Hello world!"; +} +== +

{{ hello }}

+``` The templating language provided by Winter is described in the [Markup Guide](../markup). The overall sequence the handlers are executed is described in the [Dynamic layouts](layouts#dynamic-layouts) article. diff --git a/cms-themes.md b/cms-themes.md index 6fc13d83..294e70e4 100644 --- a/cms-themes.md +++ b/cms-themes.md @@ -93,7 +93,9 @@ Winter supports single level subdirectories for **pages**, **partials**, **layou To refer to a partial or a content file from a subdirectory, specify the subdirectory's name before the template's name. Example of rendering a partial from a subdirectory: - {% partial "blog/category-list" %} +```twig +{% partial "blog/category-list" %} +``` > **NOTE:** The template paths are always absolute. If, in a partial, you render another partial from the same subdirectory, you still need to specify the subdirectory's name. @@ -104,77 +106,87 @@ Pages, partials and layout templates can include up to 3 sections: **configurati Sections are separated with the `==` sequence. For example: - url = "/blog" - layout = "default" - == - function onStart() - { - $this['posts'] = ...; - } - == -

Blog archive

- {% for post in posts %} -

{{ post.title }}

- {{ post.content }} - {% endfor %} +```twig +url = "/blog" +layout = "default" +== +function onStart() +{ + $this['posts'] = ...; +} +== +

Blog archive

+{% for post in posts %} +

{{ post.title }}

+ {{ post.content }} +{% endfor %} +``` ### Configuration section The configuration section sets the template parameters. Supported configuration parameters are specific for different CMS templates and described in their corresponding documentation articles. The configuration section uses the simple [INI format](http://en.wikipedia.org/wiki/INI_file), where string parameter values are enclosed within quotes. Example configuration section for a page template: - url = "/blog" - layout = "default" +```ini +url = "/blog" +layout = "default" - [component] - parameter = "value" +[component] +parameter = "value" +``` ### PHP code section The code in the PHP section executes every time before the template is rendered. The PHP section is optional for all CMS templates and its contents depend on the template type where it is defined. The PHP code section can contain optional open and close PHP tags to enable syntax highlighting in text editors. The open and close tags should always be specified on a different line to the section separator `==`. - url = "/blog" - layout = "default" - == - - == -

Blog archive

- {% for post in posts %} -

{{ post.title }}

- {{ post.content }} - {% endfor %} +```twig +url = "/blog" +layout = "default" +== + +== +

Blog archive

+{% for post in posts %} +

{{ post.title }}

+ {{ post.content }} +{% endfor %} +``` In the PHP section, you can only define functions and refer to namespaces with the PHP `use` keyword. No other PHP code is allowed in the PHP section. This is because the PHP section is converted to a PHP class when the page is parsed. Example of using a namespace reference: - url = "/blog" - layout = "default" - == - - == +```php +url = "/blog" +layout = "default" +== + +== +``` As a general way of setting variables, you should use the array access method on `$this`, although for simplicity you can use **object access as read-only**, for example: - // Write via array - $this['foo'] = 'bar'; +```php +// Write via array +$this['foo'] = 'bar'; - // Read via array - echo $this['foo']; +// Read via array +echo $this['foo']; - // Read-only via object - echo $this->foo; +// Read-only via object +echo $this->foo; +``` ### Twig markup section From 584a8dca5ca73d44e7ef2c680182062d81b16e0d Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Mon, 27 Dec 2021 20:59:11 +0200 Subject: [PATCH 04/26] Improving usability - console section --- console-development.md | 267 ++++++++++++++++++++++++----------------- console-scaffolding.md | 36 ++++-- 2 files changed, 185 insertions(+), 118 deletions(-) diff --git a/console-development.md b/console-development.md index e36fe476..5fae68e6 100644 --- a/console-development.md +++ b/console-development.md @@ -19,53 +19,55 @@ In addition to the provided console commands, you may also build your own custom If you wanted to create a console command called `acme:mycommand`, you might create the associated class for that command in a file called **plugins/acme/blog/console/MyCommand.php** and paste the following contents to get started: - output->writeln('Hello world!'); + } - class MyCommand extends Command + /** + * Get the console command arguments. + * @return array + */ + protected function getArguments() { - /** - * @var string The console command name. - */ - protected $name = 'acme:mycommand'; - - /** - * @var string The console command description. - */ - protected $description = 'Does something cool.'; - - /** - * Execute the console command. - * @return void - */ - public function handle() - { - $this->output->writeln('Hello world!'); - } - - /** - * Get the console command arguments. - * @return array - */ - protected function getArguments() - { - return []; - } - - /** - * Get the console command options. - * @return array - */ - protected function getOptions() - { - return []; - } + return []; + } + /** + * Get the console command options. + * @return array + */ + protected function getOptions() + { + return []; } +} +``` + Once your class is created you should fill out the `name` and `description` properties of the class, which will be used when displaying your command on the command `list` screen. The `handle` method will be called when your command is executed. You may place any command logic in this method. @@ -75,20 +77,24 @@ The `handle` method will be called when your command is executed. You may place Arguments are defined by returning an array value from the `getArguments` method are where you may define any arguments your command receives. For example: - /** - * Get the console command arguments. - * @return array - */ - protected function getArguments() - { - return [ - ['example', InputArgument::REQUIRED, 'An example argument.'], - ]; - } +```php +/** + * Get the console command arguments. + * @return array + */ +protected function getArguments() +{ + return [ + ['example', InputArgument::REQUIRED, 'An example argument.'], + ]; +} +``` When defining `arguments`, the array definition values represent the following: - array($name, $mode, $description, $defaultValue) +```php +array($name, $mode, $description, $defaultValue) +``` The argument `mode` may be any of the following: `InputArgument::REQUIRED` or `InputArgument::OPTIONAL`. @@ -97,30 +103,38 @@ The argument `mode` may be any of the following: `InputArgument::REQUIRED` or `I Options are defined by returning an array value from the `getOptions` method. Like arguments this method should return an array of commands, which are described by a list of array options. For example: - /** - * Get the console command options. - * @return array - */ - protected function getOptions() - { - return [ - ['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null], - ]; - } +```php +/** + * Get the console command options. + * @return array + */ +protected function getOptions() +{ + return [ + ['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null], + ]; +} +``` When defining `options`, the array definition values represent the following: - array($name, $shortcut, $mode, $description, $defaultValue) +```php +array($name, $shortcut, $mode, $description, $defaultValue) +``` For options, the argument `mode` may be: `InputOption::VALUE_REQUIRED`, `InputOption::VALUE_OPTIONAL`, `InputOption::VALUE_IS_ARRAY`, `InputOption::VALUE_NONE`. The `VALUE_IS_ARRAY` mode indicates that the switch may be used multiple times when calling the command: - php artisan foo --option=bar --option=baz +```bash +php artisan foo --option=bar --option=baz +``` The `VALUE_NONE` option indicates that the option is simply used as a "switch": - php artisan foo --option +```bash +php artisan foo --option +``` ### Retrieving input @@ -129,19 +143,27 @@ While your command is executing, you will obviously need to access the values fo #### Retrieving the value of a command argument - $value = $this->argument('name'); +```php +$value = $this->argument('name'); +``` #### Retrieving all arguments - $arguments = $this->argument(); +```php +$arguments = $this->argument(); +``` #### Retrieving the value of a command option - $value = $this->option('name'); +```php +$value = $this->option('name'); +``` #### Retrieving all options - $options = $this->option(); +```php +$options = $this->option(); +``` ### Writing output @@ -150,48 +172,62 @@ To send output to the console, you may use the `info`, `comment`, `question` and #### Sending information - $this->info('Display this on the screen'); +```php +$this->info('Display this on the screen'); +``` #### Sending an error message - $this->error('Something went wrong!'); +```php +$this->error('Something went wrong!'); +``` #### Asking the user for input You may also use the `ask` and `confirm` methods to prompt the user for input: - $name = $this->ask('What is your name?'); +```php +$name = $this->ask('What is your name?'); +``` #### Asking the user for secret input - $password = $this->secret('What is the password?'); +```php +$password = $this->secret('What is the password?'); +``` #### Asking the user for confirmation - if ($this->confirm('Do you wish to continue? [yes|no]')) - { - // - } +```php +if ($this->confirm('Do you wish to continue? [yes|no]')) +{ + // +} +``` You may also specify a default value to the `confirm` method, which should be `true` or `false`: - $this->confirm($question, true); +```php +$this->confirm($question, true); +``` #### Progress Bars For long running tasks, it could be helpful to show a progress indicator. Using the output object, we can start, advance and stop the Progress Bar. First, define the total number of steps the process will iterate through. Then, advance the Progress Bar after processing each item: - $users = App\User::all(); +```php +$users = App\User::all(); - $bar = $this->output->createProgressBar(count($users)); +$bar = $this->output->createProgressBar(count($users)); - foreach ($users as $user) { - $this->performTask($user); +foreach ($users as $user) { + $this->performTask($user); - $bar->advance(); - } + $bar->advance(); +} - $bar->finish(); +$bar->finish(); +``` For more advanced options, check out the [Symfony Progress Bar component documentation](https://symfony.com/doc/2.7/components/console/helpers/progressbar.html). @@ -202,54 +238,67 @@ For more advanced options, check out the [Symfony Progress Bar component documen Once your command class is finished, you need to register it so it will be available for use. This is typically done in the `register` method of a [Plugin registration file](../plugin/registration#registration-methods) using the `registerConsoleCommand` helper method. - class Blog extends PluginBase +```php +class Blog extends PluginBase +{ + public function pluginDetails() + { + [...] + } + + public function register() { - public function pluginDetails() - { - [...] - } - - public function register() - { - $this->registerConsoleCommand('acme.mycommand', 'Acme\Blog\Console\MyConsoleCommand'); - } + $this->registerConsoleCommand('acme.mycommand', 'Acme\Blog\Console\MyConsoleCommand'); } +} +``` Alternatively, plugins can supply a file named **init.php** in the plugin directory that you can use to place command registration logic. Within this file, you may use the `Artisan::add` method to register the command: - Artisan::add(new Acme\Blog\Console\MyCommand); +```php +Artisan::add(new Acme\Blog\Console\MyCommand); +``` #### Registering a command in the application container If your command is registered in the [application container](../services/application#app-container), you may use the `Artisan::resolve` method to make it available to Artisan: - Artisan::resolve('binding.name'); +```php +Artisan::resolve('binding.name'); +``` #### Registering commands in a service provider If you need to register commands from within a [service provider](../services/application#service-providers), you should call the `commands` method from the provider's `boot` method, passing the [container](../services/application#app-container) binding for the command: - public function boot() - { - $this->app->singleton('acme.mycommand', function() { - return new \Acme\Blog\Console\MyConsoleCommand; - }); +```php +public function boot() +{ + $this->app->singleton('acme.mycommand', function() { + return new \Acme\Blog\Console\MyConsoleCommand; + }); - $this->commands('acme.mycommand'); - } + $this->commands('acme.mycommand'); +} +``` ## Calling other commands Sometimes you may wish to call other commands from your command. You may do so using the `call` method: - $this->call('winter:up'); +```php +$this->call('winter:up'); +``` You can also pass arguments as an array: - $this->call('plugin:refresh', ['name' => 'Winter.Demo']); +```php +$this->call('plugin:refresh', ['name' => 'Winter.Demo']); +``` As well as options: - $this->call('winter:update', ['--force' => true]); - +```php +$this->call('winter:update', ['--force' => true]); +``` diff --git a/console-scaffolding.md b/console-scaffolding.md index bc269129..386b4814 100644 --- a/console-scaffolding.md +++ b/console-scaffolding.md @@ -21,60 +21,78 @@ Use the scaffolding commands to speed up the development process. The `create:theme` command generates a theme folder and basic files for the theme. The parameter specifies the theme code. - php artisan create:theme myauthor-mytheme +```bash +php artisan create:theme myauthor-mytheme +``` ### Create a Plugin The `create:plugin` command generates a plugin folder and basic files for the plugin. The parameter specifies the author and plugin name. - php artisan create:plugin Acme.Blog +```bash +php artisan create:plugin Acme.Blog +``` ### Create a Component The `create:component` command creates a new component class and the default component view. The first parameter specifies the author and plugin name. The second parameter specifies the component class name. - php artisan create:component Acme.Blog Post +```bash +php artisan create:component Acme.Blog Post +``` ### Create a Model The `create:model` command generates the files needed for a new model. The first parameter specifies the author and plugin name. The second parameter specifies the model class name. - php artisan create:model Acme.Blog Post +```bash +php artisan create:model Acme.Blog Post +``` ### Create a Settings Model The `create:settings` command generates the files needed for a new [Settings model](../plugin/settings#database-settings). The first parameter specifies the author and plugin name. The second parameter is optional and specifies the Settings model class name (defaults to `Settings`). - php artisan create:settings Acme.Blog CustomSettings +```bash +php artisan create:settings Acme.Blog CustomSettings +``` ### Create a backend Controller The `create:controller` command generates a controller, configuration and view files. The first parameter specifies the author and plugin name. The second parameter specifies the controller class name. - php artisan create:controller Acme.Blog Posts +```bash +php artisan create:controller Acme.Blog Posts +``` ### Create a FormWidget The `create:formwidget` command generates a backend form widget, view and basic asset files. The first parameter specifies the author and plugin name. The second parameter specifies the form widget class name. - php artisan create:formwidget Acme.Blog CategorySelector +```bash +php artisan create:formwidget Acme.Blog CategorySelector +``` ### Create a ReportWidget The `create:reportwidget` command generates a backend report widget, view and basic asset files. The first parameter specifies the author and plugin name. The second parameter specifies the report widget class name. - php artisan create:reportwidget Acme.Blog TopPosts +```bash +php artisan create:reportwidget Acme.Blog TopPosts +``` ### Create a console Command The `create:command` command generates a [new console command](../console/development). The first parameter specifies the author and plugin name. The second parameter specifies the command name. - php artisan create:command Winter.Blog MyCommand +```bash +php artisan create:command Winter.Blog MyCommand +``` From f0d7e70260ea9eabb5746c7e85c7bd8b6d590f96 Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Mon, 27 Dec 2021 22:39:04 +0200 Subject: [PATCH 05/26] Improving usability - database section --- database-attachments.md | 205 ++++---- database-basics.md | 112 +++-- database-behaviors.md | 92 ++-- database-collection.md | 176 ++++--- database-model.md | 504 +++++++++++-------- database-mutators.md | 196 ++++---- database-query.md | 651 +++++++++++++++---------- database-relations.md | 997 +++++++++++++++++++++++--------------- database-serialization.md | 118 +++-- database-structure.md | 281 ++++++----- database-traits.md | 543 ++++++++++++--------- 11 files changed, 2307 insertions(+), 1568 deletions(-) diff --git a/database-attachments.md b/database-attachments.md index 036e8d5a..ddc6343f 100644 --- a/database-attachments.md +++ b/database-attachments.md @@ -15,84 +15,111 @@ In the examples below the model has a single Avatar attachment model and many Ph A single file attachment: - public $attachOne = [ - 'avatar' => 'System\Models\File' - ]; +```php +public $attachOne = [ + 'avatar' => 'System\Models\File' +]; +``` Multiple file attachments: - public $attachMany = [ - 'photos' => 'System\Models\File' - ]; +```php +public $attachMany = [ + 'photos' => 'System\Models\File' +]; +``` > **NOTE:** If you have a column in your model's table with the same name as the attachment relationship it will not work. Attachments and the FileUpload FormWidget work using relationships, so if there is a column with the same name present in the table itself it will cause issues. Protected attachments are uploaded to the application's **uploads/protected** directory which is not accessible for the direct access from the Web. A protected file attachment is defined by setting the *public* argument to `false`: - public $attachOne = [ - 'avatar' => ['System\Models\File', 'public' => false] - ]; +```php +public $attachOne = [ + 'avatar' => ['System\Models\File', 'public' => false] +]; +``` ### Creating new attachments For singular attach relations (`$attachOne`), you may create an attachment directly via the model relationship, by setting its value using the `Input::file` method, which reads the file data from an input upload. - $model->avatar = Input::file('file_input'); +```php +$model->avatar = Input::file('file_input'); +``` You may also pass a string to the `data` attribute that contains an absolute path to a local file. - $model->avatar = '/path/to/somefile.jpg'; +```php +$model->avatar = '/path/to/somefile.jpg'; +``` Sometimes it may also be useful to create a `File` instance directly from (raw) data: - $file = (new System\Models\File)->fromData('Some content', 'sometext.txt'); +```php +$file = (new System\Models\File)->fromData('Some content', 'sometext.txt'); +``` For multiple attach relations (`$attachMany`), you may use the `create` method on the relationship instead, notice the file object is associated to the `data` attribute. This approach can be used for singular relations too, if you prefer. - $model->avatar()->create(['data' => Input::file('file_input')]); +```php +$model->avatar()->create(['data' => Input::file('file_input')]); +``` Alternatively, you can prepare a File model before hand, then manually associate the relationship later. Notice the `is_public` attribute must be set explicitly using this approach. - $file = new System\Models\File; - $file->data = Input::file('file_input'); - $file->is_public = true; - $file->save(); +```php +$file = new System\Models\File; +$file->data = Input::file('file_input'); +$file->is_public = true; +$file->save(); - $model->avatar()->add($file); +$model->avatar()->add($file); +``` You can also add a file from a URL. To work this method, you need install cURL PHP Extension. - $file = new System\Models\File; - $file->fromUrl('https://example.com/uploads/public/path/to/avatar.jpg'); +```php +$file = new System\Models\File; +$file->fromUrl('https://example.com/uploads/public/path/to/avatar.jpg'); - $user->avatar()->add($file); +$user->avatar()->add($file); +``` Occasionally you may need to change a file name. You may do so by using second method parameter. +```php $file->fromUrl('https://example.com/uploads/public/path/to/avatar.jpg', 'somefilename.jpg'); - +``` ### Viewing attachments The `getPath` method returns the full URL of an uploaded public file. The following code would print something like **example.com/uploads/public/path/to/avatar.jpg** - echo $model->avatar->getPath(); +```php +echo $model->avatar->getPath(); +``` Returning multiple attachment file paths: - foreach ($model->photos as $photo) { - echo $photo->getPath(); - } +```php +foreach ($model->photos as $photo) { + echo $photo->getPath(); +} +``` The `getLocalPath` method will return an absolute path of an uploaded file in the local filesystem. - echo $model->avatar->getLocalPath(); +```php +echo $model->avatar->getLocalPath(); +``` To output the file contents directly, use the `output` method, this will include the necessary headers for downloading the file: - echo $model->avatar->output(); +```php +echo $model->avatar->output(); +``` You can resize an image with the `getThumb` method. The method takes 3 parameters - image width, image height and the options parameter. Read more about these parameters on the [Image Resizing](../services/image-resizing#resize-parameters) page. @@ -103,70 +130,84 @@ This section shows a full usage example of the model attachments feature - from Inside your model define a relationship to the `System\Models\File` class, for example: - class Post extends Model - { - public $attachOne = [ - 'featured_image' => 'System\Models\File' - ]; - } +```php +class Post extends Model +{ + public $attachOne = [ + 'featured_image' => 'System\Models\File' + ]; +} +``` Build a form for uploading a file: - true]) ?> +```html + true]) ?> - + - + - + +``` Process the uploaded file on the server and attach it to a model: - // Find the Blog Post model - $post = Post::find(1); +```php +// Find the Blog Post model +$post = Post::find(1); - // Save the featured image of the Blog Post model - if (Input::hasFile('example_file')) { - $post->featured_image = Input::file('example_file'); - } +// Save the featured image of the Blog Post model +if (Input::hasFile('example_file')) { + $post->featured_image = Input::file('example_file'); +} +``` Alternatively, you can use [deferred binding](../database/relations#deferred-binding) to defer the relationship: - // Find the Blog Post model - $post = Post::find(1); +```php +// Find the Blog Post model +$post = Post::find(1); - // Look for the postback data 'example_file' in the HTML form above - $fileFromPost = Input::file('example_file'); +// Look for the postback data 'example_file' in the HTML form above +$fileFromPost = Input::file('example_file'); - // If it exists, save it as the featured image with a deferred session key - if ($fileFromPost) { - $post->featured_image()->create(['data' => $fileFromPost], $sessionKey); - } +// If it exists, save it as the featured image with a deferred session key +if ($fileFromPost) { + $post->featured_image()->create(['data' => $fileFromPost], $sessionKey); +} +``` Display the uploaded file on a page: - // Find the Blog Post model again - $post = Post::find(1); +```php +// Find the Blog Post model again +$post = Post::find(1); - // Look for the featured image address, otherwise use a default one - if ($post->featured_image) { - $featuredImage = $post->featured_image->getPath(); - } - else { - $featuredImage = 'http://placehold.it/220x300'; - } +// Look for the featured image address, otherwise use a default one +if ($post->featured_image) { + $featuredImage = $post->featured_image->getPath(); +} +else { + $featuredImage = 'http://placehold.it/220x300'; +} - Featured Image +Featured Image +``` If you need to access the owner of a file, you can use the `attachment` property of the `File` model: - public $morphTo = [ - 'attachment' => [] - ]; +```php +public $morphTo = [ + 'attachment' => [] +]; +``` Example: - $user = $file->attachment; +```php +$user = $file->attachment; +``` For more information read the [polymorphic relationships](../database/relations#polymorphic-relations) @@ -175,24 +216,26 @@ For more information read the [polymorphic relationships](../database/relations# The example below uses [array validation](../services/validation#validating-arrays) to validate `$attachMany` relationships. - use Winter\Storm\Database\Traits\Validation; - use System\Models\File; - use Model; +```php +use Winter\Storm\Database\Traits\Validation; +use System\Models\File; +use Model; - class Gallery extends Model - { - use Validation; +class Gallery extends Model +{ + use Validation; - public $attachMany = [ - 'photos' => File::class - ]; + public $attachMany = [ + 'photos' => File::class + ]; - public $rules = [ - 'photos' => 'required', - 'photos.*' => 'image|max:1000|dimensions:min_width=100,min_height=100' - ]; + public $rules = [ + 'photos' => 'required', + 'photos.*' => 'image|max:1000|dimensions:min_width=100,min_height=100' + ]; - /* some other code */ - } + /* some other code */ +} +``` For more information on the `attribute.*` syntax used above, see [validating arrays](../services/validation#validating-arrays). diff --git a/database-basics.md b/database-basics.md index facd7774..bfc99111 100644 --- a/database-basics.md +++ b/database-basics.md @@ -29,21 +29,23 @@ Sometimes you may wish to use one database connection for SELECT statements, and To see how read / write connections should be configured, let's look at this example: - 'mysql' => [ - 'read' => [ - 'host' => '192.168.1.1', - ], - 'write' => [ - 'host' => '196.168.1.2' - ], - 'driver' => 'mysql', - 'database' => 'database', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', - 'prefix' => '', +```php +'mysql' => [ + 'read' => [ + 'host' => '192.168.1.1', ], + 'write' => [ + 'host' => '196.168.1.2' + ], + 'driver' => 'mysql', + 'database' => 'database', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', +], +``` Note that two keys have been added to the configuration array: `read` and `write`. Both of these keys have array values containing a single key: `host`. The rest of the database options for the `read` and `write` connections will be merged from the main `mysql` array. @@ -58,81 +60,107 @@ Once you have configured your database connection, you may run queries using the To run a basic query, we can use the `select` method on the `Db` facade: - $users = Db::select('select * from users where active = ?', [1]); +```php +$users = Db::select('select * from users where active = ?', [1]); +``` The first argument passed to the `select` method is the raw SQL query, while the second argument is any parameter bindings that need to be bound to the query. Typically, these are the values of the `where` clause constraints. Parameter binding provides protection against SQL injection. The `select` method will always return an `array` of results. Each result within the array will be a PHP `stdClass` object, allowing you to access the values of the results: - foreach ($users as $user) { - echo $user->name; - } +```php +foreach ($users as $user) { + echo $user->name; +} +``` #### Using named bindings Instead of using `?` to represent your parameter bindings, you may execute a query using named bindings: - $results = Db::select('select * from users where id = :id', ['id' => 1]); +```php +$results = Db::select('select * from users where id = :id', ['id' => 1]); +``` #### Running an insert statement To execute an `insert` statement, you may use the `insert` method on the `Db` facade. Like `select`, this method takes the raw SQL query as its first argument and bindings as the second argument: - Db::insert('insert into users (id, name) values (?, ?)', [1, 'Joe']); +```php +Db::insert('insert into users (id, name) values (?, ?)', [1, 'Joe']); +``` #### Running an update statement The `update` method should be used to update existing records in the database. The number of rows affected by the statement will be returned by the method: - $affected = Db::update('update users set votes = 100 where name = ?', ['John']); +```php +$affected = Db::update('update users set votes = 100 where name = ?', ['John']); +``` #### Running a delete statement The `delete` method should be used to delete records from the database. Like `update`, the number of rows deleted will be returned: - $deleted = Db::delete('delete from users'); +```php +$deleted = Db::delete('delete from users'); +``` #### Running a general statement Some database statements should not return any value. For these types of operations, you may use the `statement` method on the `Db` facade: - Db::statement('drop table users'); +```php +Db::statement('drop table users'); +``` ## Multiple database connections When using multiple connections, you may access each connection via the `connection` method on the `Db` facade. The `name` passed to the `connection` method should correspond to one of the connections listed in your `config/database.php` configuration file: - $users = Db::connection('foo')->select(...); +```php +$users = Db::connection('foo')->select(...); +``` You may also access the raw, underlying PDO instance using the `getPdo` method on a connection instance: - $pdo = Db::connection()->getPdo(); +```php +$pdo = Db::connection()->getPdo(); +``` ## Database transactions To run a set of operations within a database transaction, you may use the `transaction` method on the `Db` facade. If an exception is thrown within the transaction `Closure`, the transaction will automatically be rolled back. If the `Closure` executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using the `transaction` method: - Db::transaction(function () { - Db::table('users')->update(['votes' => 1]); +```php +Db::transaction(function () { + Db::table('users')->update(['votes' => 1]); - Db::table('posts')->delete(); - }); + Db::table('posts')->delete(); +}); +``` #### Manually using transactions If you would like to begin a transaction manually and have complete control over rollbacks and commits, you may use the `beginTransaction` method on the `Db` facade: - Db::beginTransaction(); +```php +Db::beginTransaction(); +``` You can rollback the transaction via the `rollBack` method: - Db::rollBack(); +```php +Db::rollBack(); +``` Lastly, you can commit a transaction via the `commit` method: - Db::commit(); +```php +Db::commit(); +``` > **NOTE:** Using the `Db` facade's transaction methods also controls transactions for the [query builder](../database/query) and [model queries](../database/model). @@ -141,9 +169,11 @@ Lastly, you can commit a transaction via the `commit` method: If you would like to receive each SQL query executed by your application, you may use the `listen` method. This method is useful for logging queries or debugging. - Db::listen(function($sql, $bindings, $time) { - // - }); +```php +Db::listen(function($sql, $bindings, $time) { + // +}); +``` Just like [event registration](../services/events#event-registration), you may register your query listener in the `boot` method of a [Plugin registration file](../plugin/registration#registration-methods). Alternatively, plugins can supply a file named **init.php** in the plugin directory that you can use to place this logic. @@ -152,14 +182,20 @@ Just like [event registration](../services/events#event-registration), you may r When query logging is enabled, a log is kept in memory of all queries that have been run for the current request. Call the `enableQueryLog` method to enable this feature. - Db::connection()->enableQueryLog(); +```php +Db::connection()->enableQueryLog(); +``` To get an array of the executed queries, you may use the `getQueryLog` method: - $queries = Db::getQueryLog(); +```php +$queries = Db::getQueryLog(); +``` However, in some cases, such as when inserting a large number of rows, this can cause the application to use excess memory. To disable the log, you may use the `disableQueryLog` method: - Db::connection()->disableQueryLog(); +```php +Db::connection()->disableQueryLog(); +``` > **NOTE**: For quicker debugging it may be more useful to call the `trace_sql` [helper function](../services/error-log#helpers) instead. diff --git a/database-behaviors.md b/database-behaviors.md index bceca3dd..5d48f0c5 100644 --- a/database-behaviors.md +++ b/database-behaviors.md @@ -12,70 +12,84 @@ Purged attributes will not be saved to the database when a model is created or u attributes in your model, implement the `Winter.Storm.Database.Behaviors.Purgeable` behavior and declare a `$purgeable` property with an array containing the attributes to purge. - class User extends Model - { - public $implement = [ - 'Winter.Storm.Database.Behaviors.Purgeable' - ]; - - /** - * @var array List of attributes to purge. - */ - public $purgeable = []; - } - -You can also dynamically implement this behavior in a class. +```php +class User extends Model +{ + public $implement = [ + 'Winter.Storm.Database.Behaviors.Purgeable' + ]; /** - * Extend the Winter.User user model to implement the purgeable behavior. + * @var array List of attributes to purge. */ - Winter\User\Models\User::extend(function($model) { + public $purgeable = []; +} +``` + +You can also dynamically implement this behavior in a class. - // Implement the purgeable behavior dynamically - $model->implement[] = 'Winter.Storm.Database.Behaviors.Purgeable'; +```php +/** + * Extend the Winter.User user model to implement the purgeable behavior. + */ +Winter\User\Models\User::extend(function($model) { - // Declare the purgeable property dynamically for the purgeable behavior to use - $model->addDynamicProperty('purgeable', []); - }); + // Implement the purgeable behavior dynamically + $model->implement[] = 'Winter.Storm.Database.Behaviors.Purgeable'; + + // Declare the purgeable property dynamically for the purgeable behavior to use + $model->addDynamicProperty('purgeable', []); +}); +``` The defined attributes will be purged when the model is saved, before the [model events](#model-events) are triggered, including validation. Use the `getOriginalPurgeValue` to find a value that was purged. - return $user->getOriginalPurgeValue($propertyName); +```php +return $user->getOriginalPurgeValue($propertyName); +``` ## Sortable Sorted models will store a number value in `sort_order` which maintains the sort order of each individual model in a collection. To store a sort order for your models, implement the `Winter\Storm\Database\Behaviors\Sortable` behavior and ensure that your schema has a column defined for it to use (example: `$table->integer('sort_order')->default(0);`). - class User extends Model - { - public $implement = [ - 'Winter.Storm.Database.Behaviors.Sortable' - ]; - } +```php +class User extends Model +{ + public $implement = [ + 'Winter.Storm.Database.Behaviors.Sortable' + ]; +} +``` You can also dynamically implement this behavior in a class. - /** - * Extend the Winter.User user model to implement the sortable behavior. - */ - Winter\User\Models\User::extend(function($model) { +```php +/** + * Extend the Winter.User user model to implement the sortable behavior. + */ +Winter\User\Models\User::extend(function($model) { - // Implement the sortable behavior dynamically - $model->implement[] = 'Winter.Storm.Database.Behaviors.Sortable'; - }); + // Implement the sortable behavior dynamically + $model->implement[] = 'Winter.Storm.Database.Behaviors.Sortable'; +}); +``` You may modify the key name used to identify the sort order by defining the `SORT_ORDER` constant: - const SORT_ORDER = 'my_sort_order_column'; +```php +const SORT_ORDER = 'my_sort_order_column'; +``` Use the `setSortableOrder` method to set the orders on a single record or multiple records. - // Sets the order of the user to 1... - $user->setSortableOrder($user->id, 1); +```php +// Sets the order of the user to 1... +$user->setSortableOrder($user->id, 1); - // Sets the order of records 1, 2, 3 to 3, 2, 1 respectively... - $user->setSortableOrder([1, 2, 3], [3, 2, 1]); +// Sets the order of records 1, 2, 3 to 3, 2, 1 respectively... +$user->setSortableOrder([1, 2, 3], [3, 2, 1]); +``` > **NOTE:** If implementing this behavior in a model where data (rows) already existed previously, the data set may need to be initialized before this behavior will work correctly. To do so, either manually update each row's `sort_order` column or run a query against the data to copy the record's `id` column to the `sort_order` column (ex. `UPDATE myvendor_myplugin_mymodelrecords SET sort_order = id`). diff --git a/database-collection.md b/database-collection.md index 02536f78..e1ab2e33 100644 --- a/database-collection.md +++ b/database-collection.md @@ -15,22 +15,26 @@ All multi-result sets returned by a model are an instance of the `Illuminate\Dat All collections also serve as iterators, allowing you to loop over them as if they were simple PHP arrays: - $users = User::where('is_active', true)->get(); +```php +$users = User::where('is_active', true)->get(); - foreach ($users as $user) { - echo $user->name; - } +foreach ($users as $user) { + echo $user->name; +} +``` However, collections are much more powerful than arrays and expose a variety of map / reduce operations using an intuitive interface. For example, let's filter all active models and gather the name for each filtered user: - $users = User::get(); +```php +$users = User::get(); - $names = $users->filter(function ($user) { - return $user->is_active === true; - }) - ->map(function ($user) { - return $user->name; - }); +$names = $users->filter(function ($user) { + return $user->is_active === true; + }) + ->map(function ($user) { + return $user->name; + }); +``` > **NOTE:** While most model collection methods return a new instance of an `Eloquent` collection, the `pluck`, `keys`, `zip`, `collapse`, `flatten` and `flip` methods return a base collection instance. Likewise, if a `map` operation returns a collection that does not contain any models, it will be automatically cast to a base collection. @@ -46,119 +50,149 @@ In addition, the `Illuminate\Database\Eloquent\Collection` class provides a supe The `contains` method may be used to determine if a given model instance is contained by the collection. This method accepts a primary key or a model instance: - $users->contains(1); +```php +$users->contains(1); - $users->contains(User::find(1)); +$users->contains(User::find(1)); +``` **diff($items)** The `diff` method returns all of the models that are not present in the given collection: - use App\User; +```php +use App\User; - $users = $users->diff(User::whereIn('id', [1, 2, 3])->get()); +$users = $users->diff(User::whereIn('id', [1, 2, 3])->get()); +``` **except($keys)** The `except` method returns all of the models that do not have the given primary keys: - $users = $users->except([1, 2, 3]); +```php +$users = $users->except([1, 2, 3]); +``` **find($key)** The `find` method finds a model that has a given primary key. If `$key` is a model instance, `find` will attempt to return a model matching the primary key. If `$key` is an array of keys, find will return all models which match the `$keys` using `whereIn()`: - $users = User::all(); +```php +$users = User::all(); - $user = $users->find(1); +$user = $users->find(1); +``` **fresh($with = [])** The `fresh` method retrieves a fresh instance of each model in the collection from the database. In addition, any specified relationships will be eager loaded: - $users = $users->fresh(); +```php +$users = $users->fresh(); - $users = $users->fresh('comments'); +$users = $users->fresh('comments'); +``` **intersect($items)** The `intersect` method returns all of the models that are also present in the given collection: - use App\User; +```php +use App\User; - $users = $users->intersect(User::whereIn('id', [1, 2, 3])->get()); +$users = $users->intersect(User::whereIn('id', [1, 2, 3])->get()); +``` **load($relations)** The `load` method eager loads the given relationships for all models in the collection: - $users->load('comments', 'posts'); +```php +$users->load('comments', 'posts'); - $users->load('comments.author'); +$users->load('comments.author'); +``` **loadMissing($relations)** The `loadMissing` method eager loads the given relationships for all models in the collection if the relationships are not already loaded: - $users->loadMissing('comments', 'posts'); +```php +$users->loadMissing('comments', 'posts'); - $users->loadMissing('comments.author'); +$users->loadMissing('comments.author'); +``` **modelKeys()** The `modelKeys` method returns the primary keys for all models in the collection: - $users->modelKeys(); +```php +$users->modelKeys(); - // [1, 2, 3, 4, 5] +// [1, 2, 3, 4, 5] +``` **makeVisible($attributes)** The `makeVisible` method makes attributes visible that are typically "hidden" on each model in the collection: - $users = $users->makeVisible(['address', 'phone_number']); +```php +$users = $users->makeVisible(['address', 'phone_number']); +``` **makeHidden($attributes)** The `makeHidden` method hides attributes that are typically "visible" on each model in the collection: - $users = $users->makeHidden(['address', 'phone_number']); +```php +$users = $users->makeHidden(['address', 'phone_number']); +``` **only($keys)** The `only` method returns all of the models that have the given primary keys: - $users = $users->only([1, 2, 3]); +```php +$users = $users->only([1, 2, 3]); +``` **unique($key = null, $strict = false)** The `unique` method returns all of the unique models in the collection. Any models of the same type with the same primary key as another model in the collection are removed. - $users = $users->unique(); +```php +$users = $users->unique(); +``` ## Custom collections If you need to use a custom `Collection` object with your own extension methods, you may override the `newCollection` method on your model: - class User extends Model +```php +class User extends Model +{ + /** + * Create a new Collection instance. + */ + public function newCollection(array $models = []) { - /** - * Create a new Collection instance. - */ - public function newCollection(array $models = []) - { - return new CustomCollection($models); - } + return new CustomCollection($models); } +} +``` Once you have defined a `newCollection` method, you will receive an instance of your custom collection anytime the model returns a `Collection` instance. If you would like to use a custom collection for every model in your plugin or application, you should override the `newCollection` method on a model base class that is extended by all of your models. - use Winter\Storm\Database\Collection as CollectionBase; +```php +use Winter\Storm\Database\Collection as CollectionBase; - class CustomCollection extends CollectionBase - { - } +class CustomCollection extends CollectionBase +{ +} +``` ## Data feed @@ -172,48 +206,54 @@ The `DataFeed` class mimics a regular model and supports `limit` and `paginate` The next example will combine the User, Post and Comment models in to a single collection and returns the first 10 records. - $feed = new Winter\Storm\Database\DataFeed; - $feed->add('user', new User); - $feed->add('post', Post::where('category_id', 7)); +```php +$feed = new Winter\Storm\Database\DataFeed; +$feed->add('user', new User); +$feed->add('post', Post::where('category_id', 7)); - $feed->add('comment', function() { - $comment = new Comment; - return $comment->where('approved', true); - }); +$feed->add('comment', function() { + $comment = new Comment; + return $comment->where('approved', true); +}); - $results = $feed->limit(10)->get(); +$results = $feed->limit(10)->get(); +``` ### Processing results The `get` method will return a `Collection` object that contains the results. Records can be differentiated by using the `tag_name` attribute which was set as the first parameter when the model was added. - foreach ($results as $result) { +```php +foreach ($results as $result) { - if ($result->tag_name == 'post') - echo "New Blog Post: " . $record->title; + if ($result->tag_name == 'post') + echo "New Blog Post: " . $record->title; - elseif ($result->tag_name == 'comment') - echo "New Comment: " . $record->content; + elseif ($result->tag_name == 'comment') + echo "New Comment: " . $record->content; - elseif ($result->tag_name == 'user') - echo "New User: " . $record->name; + elseif ($result->tag_name == 'user') + echo "New User: " . $record->name; - } +} +``` ### Ordering results Results can be ordered by a single database column, either shared default used by all datasets or individually specified with the `add` method. The direction of results must also be shared. - // Ordered by updated_at if it exists, otherwise created_at - $feed->add('user', new User, 'ifnull(updated_at, created_at)'); +```php +// Ordered by updated_at if it exists, otherwise created_at +$feed->add('user', new User, 'ifnull(updated_at, created_at)'); - // Ordered by id - $feed->add('comments', new Comment, 'id'); +// Ordered by id +$feed->add('comments', new Comment, 'id'); - // Ordered by name (specified default below) - $feed->add('posts', new Post); +// Ordered by name (specified default below) +$feed->add('posts', new Post); - // Specifies the default column and the direction - $feed->orderBy('name', 'asc')->get(); +// Specifies the default column and the direction +$feed->orderBy('name', 'asc')->get(); +``` diff --git a/database-model.md b/database-model.md index a16c8778..e600ef32 100644 --- a/database-model.md +++ b/database-model.md @@ -42,19 +42,21 @@ The model configuration directory could contain the model's [list column](../bac In most cases, you should create one model class for each database table. All model classes must extend the `Model` class. The most basic representation of a model used inside a Plugin looks like this: - namespace Acme\Blog\Models; +```php +namespace Acme\Blog\Models; - use Model; +use Model; - class Post extends Model - { - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'acme_blog_posts'; - } +class Post extends Model +{ + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'acme_blog_posts'; +} +``` The `$table` protected field specifies the database table corresponding the model. The table name is a snake case name of the author, plugin and pluralized record type name. @@ -63,20 +65,22 @@ The `$table` protected field specifies the database table corresponding the mode There are some standard properties that can be found on models, in addition to those provided by [model traits](traits). For example: - class User extends Model - { - protected $primaryKey = 'id'; +```php +class User extends Model +{ + protected $primaryKey = 'id'; - public $exists = false; + public $exists = false; - protected $dates = ['last_seen_at']; + protected $dates = ['last_seen_at']; - public $timestamps = true; + public $timestamps = true; - protected $jsonable = ['permissions']; + protected $jsonable = ['permissions']; - protected $guarded = ['*']; - } + protected $guarded = ['*']; +} +``` Property | Description ------------- | ------------- @@ -97,70 +101,80 @@ Property | Description Models will assume that each table has a primary key column named `id`. You may define a `$primaryKey` property to override this convention. - class Post extends Model - { - /** - * The primary key for the model. - * - * @var string - */ - protected $primaryKey = 'id'; - } +```php +class Post extends Model +{ + /** + * The primary key for the model. + * + * @var string + */ + protected $primaryKey = 'id'; +} +``` #### Incrementing Models will assume that the primary key is an incrementing integer value, which means that by default the primary key will be cast to an integer automatically. If you wish to use a non-incrementing or a non-numeric primary key you must set the public `$incrementing` property to false. - class Message extends Model - { - /** - * The primary key for the model is not an integer. - * - * @var bool - */ - public $incrementing = false; - } +```php +class Message extends Model +{ + /** + * The primary key for the model is not an integer. + * + * @var bool + */ + public $incrementing = false; +} +``` #### Timestamps By default, a model will expect `created_at` and `updated_at` columns to exist on your tables. If you do not wish to have these columns managed automatically, set the `$timestamps` property on your model to `false`: - class Post extends Model - { - /** - * Indicates if the model should be timestamped. - * - * @var bool - */ - public $timestamps = false; - } +```php +class Post extends Model +{ + /** + * Indicates if the model should be timestamped. + * + * @var bool + */ + public $timestamps = false; +} +``` If you need to customize the format of your timestamps, set the `$dateFormat` property on your model. This property determines how date attributes are stored in the database, as well as their format when the model is serialized to an array or JSON: - class Post extends Model - { - /** - * The storage format of the model's date columns. - * - * @var string - */ - protected $dateFormat = 'U'; - } +```php +class Post extends Model +{ + /** + * The storage format of the model's date columns. + * + * @var string + */ + protected $dateFormat = 'U'; +} +``` #### Values stored as JSON When attributes names are passed to the `$jsonable` property, the values will be serialized and deserialized from the database as JSON: - class Post extends Model - { - /** - * @var array Attribute names to encode and decode using JSON. - */ - protected $jsonable = ['data']; - } +```php +class Post extends Model +{ + /** + * @var array Attribute names to encode and decode using JSON. + */ + protected $jsonable = ['data']; +} +``` ## Retrieving models @@ -174,26 +188,32 @@ When requesting data from the database the model will retrieve values primarily Once you have created a model and [its associated database table](../database/structure#migration-structure), you are ready to start retrieving data from your database. Think of each model as a powerful [query builder](../database/query) allowing you to query the database table associated with the model. For example: - $flights = Flight::all(); +```php +$flights = Flight::all(); +``` #### Accessing column values If you have a model instance, you may access the column values of the model by accessing the corresponding property. For example, let's loop through each `Flight` instance returned by our query and echo the value of the `name` column: - foreach ($flights as $flight) { - echo $flight->name; - } +```php +foreach ($flights as $flight) { + echo $flight->name; +} +``` #### Adding additional constraints The `all` method will return all of the results in the model's table. Since each model serves as a [query builder](../database/query), you may also add constraints to queries, and then use the `get` method to retrieve the results: - $flights = Flight::where('active', 1) - ->orderBy('name', 'desc') - ->take(10) - ->get(); +```php +$flights = Flight::where('active', 1) + ->orderBy('name', 'desc') + ->take(10) + ->get(); +``` > **NOTE:** Since models are query builders, you should familiarize yourself with all of the methods available on the [query builder](../database/query). You may use any of these methods in your model queries. @@ -202,20 +222,24 @@ The `all` method will return all of the results in the model's table. Since each For methods like `all` and `get` which retrieve multiple results, an instance of a `Collection` will be returned. This class provides [a variety of helpful methods](../database/collection) for working with your results. Of course, you can simply loop over this collection like an array: - foreach ($flights as $flight) { - echo $flight->name; - } +```php +foreach ($flights as $flight) { + echo $flight->name; +} +``` #### Chunking results If you need to process thousands of records, use the `chunk` command. The `chunk` method will retrieve a "chunk" of models, feeding them to a given `Closure` for processing. Using the `chunk` method will conserve memory when working with large result sets: - Flight::chunk(200, function ($flights) { - foreach ($flights as $flight) { - // - } - }); +```php +Flight::chunk(200, function ($flights) { + foreach ($flights as $flight) { + // + } +}); +``` The first argument passed to the method is the number of records you wish to receive per "chunk". The Closure passed as the second argument will be called for each chunk that is retrieved from the database. @@ -224,35 +248,43 @@ The first argument passed to the method is the number of records you wish to rec In addition to retrieving all of the records for a given table, you may also retrieve single records using `find` and `first`. Instead of returning a collection of models, these methods return a single model instance: - // Retrieve a model by its primary key - $flight = Flight::find(1); +```php +// Retrieve a model by its primary key +$flight = Flight::find(1); - // Retrieve the first model matching the query constraints - $flight = Flight::where('active', 1)->first(); +// Retrieve the first model matching the query constraints +$flight = Flight::where('active', 1)->first(); +``` #### Not found exceptions Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The `findOrFail` and `firstOrFail` methods will retrieve the first result of the query. However, if no result is found, a `Illuminate\Database\Eloquent\ModelNotFoundException` will be thrown: - $model = Flight::findOrFail(1); +```php +$model = Flight::findOrFail(1); - $model = Flight::where('legs', '>', 100)->firstOrFail(); +$model = Flight::where('legs', '>', 100)->firstOrFail(); +``` When [developing an API](../services/router), if the exception is not caught, a `404` HTTP response is automatically sent back to the user, so it is not necessary to write explicit checks to return `404` responses when using these methods: - Route::get('/api/flights/{id}', function ($id) { - return Flight::findOrFail($id); - }); +```php +Route::get('/api/flights/{id}', function ($id) { + return Flight::findOrFail($id); +}); +``` ### Retrieving aggregates You may also use `count`, `sum`, `max`, and other [aggregate functions](../database/query#aggregates) provided by the query builder. These methods return the appropriate scalar value instead of a full model instance: - $count = Flight::where('active', 1)->count(); +```php +$count = Flight::where('active', 1)->count(); - $max = Flight::where('active', 1)->max('price'); +$max = Flight::where('active', 1)->max('price'); +``` ## Inserting & updating models @@ -264,9 +296,11 @@ Inserting and updating data are the cornerstone feature of models, it makes the To create a new record in the database, simply create a new model instance, set attributes on the model, then call the `save` method: - $flight = new Flight; - $flight->name = 'Sydney to Canberra'; - $flight->save(); +```php +$flight = new Flight; +$flight->name = 'Sydney to Canberra'; +$flight->save(); +``` In this example, we simply create a new instance of the `Flight` model and assign the `name` attribute. When we call the `save` method, a record will be inserted into the database. The `created_at` and `updated_at` timestamps will automatically be set too, so there is no need to set them manually. @@ -275,15 +309,19 @@ In this example, we simply create a new instance of the `Flight` model and assig The `save` method may also be used to update models that already exist in the database. To update a model, you should retrieve it, set any attributes you wish to update, and then call the `save` method. Again, the `updated_at` timestamp will automatically be updated, so there is no need to manually set its value: - $flight = Flight::find(1); - $flight->name = 'Darwin to Adelaide'; - $flight->save(); +```php +$flight = Flight::find(1); +$flight->name = 'Darwin to Adelaide'; +$flight->save(); +``` Updates can also be performed against any number of models that match a given query. In this example, all flights that are `active` and have a `destination` of `San Diego` will be marked as delayed: - Flight::where('is_active', true) - ->where('destination', 'Perth') - ->update(['delayed' => true]); +```php +Flight::where('is_active', true) + ->where('destination', 'Perth') + ->update(['delayed' => true]); +``` The `update` method expects an array of column and value pairs representing the columns that should be updated. @@ -291,10 +329,12 @@ The `update` method expects an array of column and value pairs representing the If you would like to perform multiple "upserts" in a single query, then you should use the `upsert` method instead. The method's first argument consists of the values to insert or update, while the second argument lists the column(s) that uniquely identify records within the associated table. The method's third and final argument is an array of the columns that should be updated if a matching record already exists in the database. The `upsert` method will automatically set the `created_at` and `updated_at` timestamps if timestamps are enabled on the model: - MyVendor\MyPlugin\Models\Flight::upsert([ - ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], - ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] - ], ['departure', 'destination'], ['price']); +```php +MyVendor\MyPlugin\Models\Flight::upsert([ + ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], + ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] +], ['departure', 'destination'], ['price']); +``` > **NOTE::** All databases except SQL Server require the columns in the second argument of the `upsert` method to have a "primary" or "unique" index. @@ -307,31 +347,37 @@ A mass-assignment vulnerability occurs when a user passes an unexpected HTTP par To get started, you should define which model attributes you want to make mass assignable. You may do this using the `$fillable` property on the model. For example, let's make the `name` attribute of our `Flight` model mass assignable: - class Flight extends Model - { - /** - * The attributes that are mass assignable. - * - * @var array - */ - protected $fillable = ['name']; - } +```php +class Flight extends Model +{ + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = ['name']; +} +``` Once we have made the attributes mass assignable, we can use the `create` method to insert a new record in the database. The `create` method returns the saved model instance: - $flight = Flight::create(['name' => 'Flight 10']); +```php +$flight = Flight::create(['name' => 'Flight 10']); +``` While `$fillable` serves as a "white list" of attributes that should be mass assignable, you may also choose to use `$guarded`. The `$guarded` property should contain an array of attributes that you do not want to be mass assignable. All other attributes not in the array will be mass assignable. So, `$guarded` functions like a "black list". Of course, you should use either `$fillable` or `$guarded` - not both: - class Flight extends Model - { - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = ['price']; - } +```php +class Flight extends Model +{ + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = ['price']; +} +``` In the example above, all attributes **except for `price`** will be mass assignable. @@ -339,46 +385,56 @@ In the example above, all attributes **except for `price`** will be mass assigna Sometimes you may wish to only instantiate a new instance of a model. You can do this using the `make` method. The `make` method will simply return a new instance without saving or creating anything. - $flight = Flight::make(['name' => 'Flight 10']); +```php +$flight = Flight::make(['name' => 'Flight 10']); - // Functionally the same as... - $flight = new Flight; - $flight->fill(['name' => 'Flight 10']); +// Functionally the same as... +$flight = new Flight; +$flight->fill(['name' => 'Flight 10']); +``` There are two other methods you may use to create models by mass assigning attributes: `firstOrCreate` and `firstOrNew`. The `firstOrCreate` method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the given attributes. The `firstOrNew` method, like `firstOrCreate` will attempt to locate a record in the database matching the given attributes. However, if a model is not found, a new model instance will be returned. Note that the model returned by `firstOrNew` has not yet been persisted to the database. You will need to call `save` manually to persist it: - // Retrieve the flight by the attributes, otherwise create it - $flight = Flight::firstOrCreate(['name' => 'Flight 10']); +```php +// Retrieve the flight by the attributes, otherwise create it +$flight = Flight::firstOrCreate(['name' => 'Flight 10']); - // Retrieve the flight by the attributes, or instantiate a new instance - $flight = Flight::firstOrNew(['name' => 'Flight 10']); +// Retrieve the flight by the attributes, or instantiate a new instance +$flight = Flight::firstOrNew(['name' => 'Flight 10']); +``` ## Deleting models To delete a model, call the `delete` method on a model instance: - $flight = Flight::find(1); +```php +$flight = Flight::find(1); - $flight->delete(); +$flight->delete(); +``` #### Deleting an existing model by key In the example above, we are retrieving the model from the database before calling the `delete` method. However, if you know the primary key of the model, you may delete the model without retrieving it. To do so, call the `destroy` method: - Flight::destroy(1); +```php +Flight::destroy(1); - Flight::destroy([1, 2, 3]); +Flight::destroy([1, 2, 3]); - Flight::destroy(1, 2, 3); +Flight::destroy(1, 2, 3); +``` #### Deleting models by query You may also run a delete query on a set of models. In this example, we will delete all flights that are marked as inactive: - $deletedRows = Flight::where('active', 0)->delete(); +```php +$deletedRows = Flight::where('active', 0)->delete(); +``` > **NOTE**: It is important to mention that [model events](#model-events) will not fire when deleting records directly from a query. @@ -390,50 +446,57 @@ You may also run a delete query on a set of models. In this example, we will del Scopes allow you to define common sets of constraints that you may easily re-use throughout your application. For example, you may need to frequently retrieve all users that are considered "popular". To define a scope, simply prefix a model method with `scope`: - class User extends Model +```php +class User extends Model +{ + /** + * Scope a query to only include popular users. + */ + public function scopePopular($query) + { + return $query->where('votes', '>', 100); + } + + /** + * Scope a query to only include active users. + */ + public function scopeActive($query) { - /** - * Scope a query to only include popular users. - */ - public function scopePopular($query) - { - return $query->where('votes', '>', 100); - } - - /** - * Scope a query to only include active users. - */ - public function scopeActive($query) - { - return $query->where('is_active', 1); - } + return $query->where('is_active', 1); } +} +``` #### Utilizing a query scope Once the scope has been defined, you may call the scope methods when querying the model. However, you do not need to include the `scope` prefix when calling the method. You can even chain calls to various scopes, for example: - $users = User::popular()->active()->orderBy('created_at')->get(); +```php +$users = User::popular()->active()->orderBy('created_at')->get(); +``` #### Dynamic scopes Sometimes you may wish to define a scope that accepts parameters. To get started, just add your additional parameters to your scope. Scope parameters should be defined after the `$query` argument: - class User extends Model +```php +class User extends Model +{ + /** + * Scope a query to only include users of a given type. + */ + public function scopeApplyType($query, $type) { - /** - * Scope a query to only include users of a given type. - */ - public function scopeApplyType($query, $type) - { - return $query->where('type', $type); - } + return $query->where('type', $type); } +} +``` Now you may pass the parameters when calling the scope: - $users = User::applyType('admin')->get(); - +```php +$users = User::applyType('admin')->get(); +``` #### Global scopes @@ -446,6 +509,7 @@ Global scopes allow you to add constraints to all queries for a given model. Win Writing a global scope is simple. First, define a class that implements the `Illuminate\Database\Eloquent\Scope` interface. Winter does not have a conventional location that you should place scope classes, so you are free to place this class in any directory that you wish. The `Scope` interface requires you to implement one method: `apply`. The `apply` method may add `where` constraints or other types of clauses to the query as needed: + ```php slug = Str::slug($this->name); - } +```php +/** + * Generate a URL slug for this model + */ +public function beforeCreate() +{ + $this->slug = Str::slug($this->name); +} +``` > **NOTE:** Relationships created with [deferred-binding](relations#deferred-binding) (i.e: file attachments) will not be available in the `afterSave` model event if they have not been committed yet. To access uncommitted bindings, use the `withDeferred($sessionKey)` method on the relation. Example: `$this->images()->withDeferred(post('_session_key'))->get();` @@ -600,38 +666,46 @@ Whenever a new model is saved for the first time, the `beforeCreate` and `afterC For example, let's define an event listener that populates the slug attribute when a model is first created: - /** - * Generate a URL slug for this model - */ - public function beforeCreate() - { - $this->slug = Str::slug($this->name); - } +```php +/** + * Generate a URL slug for this model + */ +public function beforeCreate() +{ + $this->slug = Str::slug($this->name); +} +``` Returning `false` from an event will cancel the `save` / `update` operation: - public function beforeCreate() - { - if (!$user->isValid()) { - return false; - } +```php +public function beforeCreate() +{ + if (!$user->isValid()) { + return false; } +} +``` It's possible to access old values using the `original` attribute. For example: - public function afterUpdate() - { - if ($this->title != $this->original['title']) { - // title changed - } +```php +public function afterUpdate() +{ + if ($this->title != $this->original['title']) { + // title changed } +} +``` You can externally bind to [local events](../services/events) for a single instance of a model using the `bindEvent` method. The event name should be the same as the method override name, prefixed with `model.`. - $flight = new Flight; - $flight->bindEvent('model.beforeCreate', function() use ($model) { - $model->slug = Str::slug($model->name); - }) +```php +$flight = new Flight; +$flight->bindEvent('model.beforeCreate', function() use ($model) { + $model->slug = Str::slug($model->name); +}) +``` ## Extending models @@ -640,33 +714,39 @@ Since models are [equipped to use behaviors](../services/behaviors), they can be Inside the closure you can add relations to the model. Here we extend the `Backend\Models\User` model to include a profile (has one) relationship referencing the `Acme\Demo\Models\Profile` model. - \Backend\Models\User::extend(function($model) { - $model->hasOne['profile'] = ['Acme\Demo\Models\Profile', 'key' => 'user_id']; - }); +```php +\Backend\Models\User::extend(function($model) { + $model->hasOne['profile'] = ['Acme\Demo\Models\Profile', 'key' => 'user_id']; +}); +``` This approach can also be used to bind to [local events](#events), the following code listens for the `model.beforeSave` event. - \Backend\Models\User::extend(function($model) { - $model->bindEvent('model.beforeSave', function() use ($model) { - // ... - }); +```php +\Backend\Models\User::extend(function($model) { + $model->bindEvent('model.beforeSave', function() use ($model) { + // ... }); +}); +``` > **NOTE:** Typically the best place to place code is within your plugin registration class `boot` method as this will be run on every request ensuring that the extensions you make to the model are available everywhere. Additionally, a few methods exist to extend protected model properties. - \Backend\Models\User::extend(function($model) { - // add cast attributes - $model->addCasts([ - 'some_extended_field' => 'int', - ]); - - // add a date attribute - $model->addDateAttribute('updated_at'); - - // add fillable or jsonable fields - // these methods accept one or more strings, or an array of strings - $model->addFillable('first_name'); - $model->addJsonable('some_data'); - }); +```php +\Backend\Models\User::extend(function($model) { + // add cast attributes + $model->addCasts([ + 'some_extended_field' => 'int', + ]); + + // add a date attribute + $model->addDateAttribute('updated_at'); + + // add fillable or jsonable fields + // these methods accept one or more strings, or an array of strings + $model->addFillable('first_name'); + $model->addJsonable('some_data'); +}); +``` diff --git a/database-mutators.md b/database-mutators.md index 0bba8146..d7aa52fd 100644 --- a/database-mutators.md +++ b/database-mutators.md @@ -19,57 +19,65 @@ In addition to custom accessors and mutators, you can also automatically cast da To define an accessor, create a `getFooAttribute` method on your model where `Foo` is the "camel" cased name of the column you wish to access. In this example, we'll define an accessor for the `first_name` attribute. The accessor will automatically be called when attempting to retrieve the value of `first_name`: - first_name; +$firstName = $user->first_name; +``` #### Defining a mutator To define a mutator, define a `setFooAttribute` method on your model where `Foo` is the "camel" cased name of the column you wish to access. In this example, let's define a mutator for the `first_name` attribute. This mutator will be automatically called when we attempt to set the value of the `first_name` attribute on the model: - attributes['first_name'] = strtolower($value); - } + $this->attributes['first_name'] = strtolower($value); } +} +``` The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the model's internal `$attributes` property. For example, if we attempt to set the `first_name` attribute to `Sally`: - $user = User::find(1); +```php +$user = User::find(1); - $user->first_name = 'Sally'; +$user->first_name = 'Sally'; +``` Here the `setFirstNameAttribute` function will be called with the value `Sally`. The mutator will then apply the `strtolower` function to the name and set its value in the internal `$attributes` array. @@ -80,41 +88,49 @@ By default, Models in Winter will convert the `created_at` and `updated_at` colu You may customize which fields are automatically mutated, and even completely disable this mutation, by overriding the `$dates` property of your model: - class User extends Model - { - /** - * The attributes that should be mutated to dates. - * - * @var array - */ - protected $dates = ['created_at', 'updated_at', 'disabled_at']; - } +```php +class User extends Model +{ + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['created_at', 'updated_at', 'disabled_at']; +} +``` When a column is considered a date, you may set its value to a UNIX timestamp, date string (`Y-m-d`), date-time string, and of course a `DateTime` / `Carbon` instance, and the date's value will automatically be correctly stored in your database: - $user = User::find(1); +```php +$user = User::find(1); - $user->disabled_at = Carbon::now(); +$user->disabled_at = Carbon::now(); - $user->save(); +$user->save(); +``` As noted above, when retrieving attributes that are listed in your `$dates` property, they will automatically be cast to [Carbon](https://github.com/briannesbitt/Carbon) instances, allowing you to use any of Carbon's methods on your attributes: - $user = User::find(1); +```php +$user = User::find(1); - return $user->disabled_at->getTimestamp(); +return $user->disabled_at->getTimestamp(); +``` By default, timestamps are formatted as `'Y-m-d H:i:s'`. If you need to customize the timestamp format, set the `$dateFormat` property on your model. This property determines how date attributes are stored in the database, as well as their format when the model is serialized to an array or JSON: - class Flight extends Model - { - /** - * The storage format of the model's date columns. - * - * @var string - */ - protected $dateFormat = 'U'; - } +```php +class Flight extends Model +{ + /** + * The storage format of the model's date columns. + * + * @var string + */ + protected $dateFormat = 'U'; +} +``` ## Attribute casting @@ -123,50 +139,58 @@ The `$casts` property on your model provides a convenient method of converting a For example, let's cast the `is_admin` attribute, which is stored in our database as an integer (`0` or `1`) to a boolean value: - class User extends Model - { - /** - * The attributes that should be casted to native types. - * - * @var array - */ - protected $casts = [ - 'is_admin' => 'boolean', - ]; - } +```php +class User extends Model +{ + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts = [ + 'is_admin' => 'boolean', + ]; +} +``` Now the `is_admin` attribute will always be cast to a boolean when you access it, even if the underlying value is stored in the database as an integer: - $user = User::find(1); +```php +$user = User::find(1); - if ($user->is_admin) { - // - } +if ($user->is_admin) { + // +} +``` #### Array casting The `array` cast type is particularly useful when working with columns that are stored as serialized JSON. For example, if your database has a `TEXT` field type that contains serialized JSON, adding the `array` cast to that attribute will automatically deserialize the attribute to a PHP array when you access it on your Eloquent model: - class User extends Model - { - /** - * The attributes that should be casted to native types. - * - * @var array - */ - protected $casts = [ - 'options' => 'array', - ]; - } +```php +class User extends Model +{ + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts = [ + 'options' => 'array', + ]; +} +``` Once the cast is defined, you may access the `options` attribute and it will automatically be deserialized from JSON into a PHP array. When you set the value of the `options` attribute, the given array will automatically be serialized back into JSON for storage: - $user = User::find(1); +```php +$user = User::find(1); - $options = $user->options; +$options = $user->options; - $options['key'] = 'value'; +$options['key'] = 'value'; - $user->options = $options; +$user->options = $options; - $user->save(); +$user->save(); +``` diff --git a/database-query.md b/database-query.md index 2421368e..9fe754ec 100644 --- a/database-query.md +++ b/database-query.md @@ -34,74 +34,91 @@ The database query builder provides a convenient, fluent interface to creating a To begin a fluent query, use the `table` method on the `Db` facade. The `table` method returns a fluent query builder instance for the given table, allowing you to chain more constraints onto the query and then finally get the results. In this example, let's just `get` all records from a table: - $users = Db::table('users')->get(); +```php +$users = Db::table('users')->get(); +``` Like [raw queries](../database/basics#running-queries), the `get` method returns an `array` of results where each result is an instance of the PHP `stdClass` object. You may access each column's value by accessing the column as a property of the object: - foreach ($users as $user) { - echo $user->name; - } +```php +foreach ($users as $user) { + echo $user->name; +} +``` #### Retrieving a single row / column from a table If you just need to retrieve a single row from the database table, you may use the `first` method. This method will return a single `stdClass` object: - $user = Db::table('users')->where('name', 'John')->first(); +```php +$user = Db::table('users')->where('name', 'John')->first(); - echo $user->name; +echo $user->name; +``` If you don't even need an entire row, you may extract a single value from a record using the `value` method. This method will return the value of the column directly: - $email = Db::table('users')->where('name', 'John')->value('email'); - +```php +$email = Db::table('users')->where('name', 'John')->value('email'); +``` #### Retrieving a list of column values If you would like to retrieve an array containing the values of a single column, you may use the `lists` method. In this example, we'll retrieve an array of role titles: - $titles = Db::table('roles')->lists('title'); +```php +$titles = Db::table('roles')->lists('title'); - foreach ($titles as $title) { - echo $title; - } +foreach ($titles as $title) { + echo $title; +} +``` You may also specify a custom key column for the returned array: - $roles = Db::table('roles')->lists('title', 'name'); +```php +$roles = Db::table('roles')->lists('title', 'name'); - foreach ($roles as $name => $title) { - echo $title; - } +foreach ($roles as $name => $title) { + echo $title; +} +``` ### Chunking results If you need to work with thousands of database records, consider using the `chunk` method. This method retrieves a small "chunk" of the results at a time, and feeds each chunk into a `Closure` for processing. This method is very useful for writing [console commands](../console/development) that process thousands of records. For example, let's work with the entire `users` table in chunks of 100 records at a time: - Db::table('users')->chunk(100, function($users) { - foreach ($users as $user) { - // - } - }); +```php +Db::table('users')->chunk(100, function($users) { + foreach ($users as $user) { + // + } +}); +``` You may stop further chunks from being processed by returning `false` from the `Closure`: - Db::table('users')->chunk(100, function($users) { - // Process the records... +```php +Db::table('users')->chunk(100, function($users) { + // Process the records... - return false; - }); + return false; +}); +``` If you are updating database records while chunking results, your chunk results could change in unexpected ways. So, when updating records while chunking, it is always best to use the `chunkById` method instead. This method will automatically paginate the results based on the record's primary key: - Db::table('users')->where('active', false) - ->chunkById(100, function ($users) { - foreach ($users as $user) { - Db::table('users') - ->where('id', $user->id) - ->update(['active' => true]); - } - }); +```php +Db::table('users')->where('active', false) + ->chunkById(100, function ($users) { + foreach ($users as $user) { + Db::table('users') + ->where('id', $user->id) + ->update(['active' => true]); + } + }); +``` > **NOTE:** When updating or deleting records inside the chunk callback, any changes to the primary key or foreign keys could affect the chunk query. This could potentially result in records not being included in the chunked results. @@ -110,23 +127,29 @@ If you are updating database records while chunking results, your chunk results The query builder also provides a variety of aggregate methods, such as `count`, `max`, `min`, `avg`, and `sum`. You may call any of these methods after constructing your query: - $users = Db::table('users')->count(); +```php +$users = Db::table('users')->count(); - $price = Db::table('orders')->max('price'); +$price = Db::table('orders')->max('price'); +``` Of course, you may combine these methods with other clauses to build your query: - $price = Db::table('orders') - ->where('is_finalized', 1) - ->avg('price'); +```php +$price = Db::table('orders') + ->where('is_finalized', 1) + ->avg('price'); +``` #### Determining if records exist Instead of using the `count` method to determine if any records exist that match your query's constraints, you may use the `exists` and `doesntExist` methods: - return Db::table('orders')->where('finalized', 1)->exists(); +```php +return Db::table('orders')->where('finalized', 1)->exists(); - return Db::table('orders')->where('finalized', 1)->doesntExist(); +return Db::table('orders')->where('finalized', 1)->doesntExist(); +``` ## Selects @@ -135,33 +158,43 @@ Instead of using the `count` method to determine if any records exist that match Of course, you may not always want to select all columns from a database table. Using the `select` method, you can specify a custom `select` clause for the query: - $users = Db::table('users')->select('name', 'email as user_email')->get(); +```php +$users = Db::table('users')->select('name', 'email as user_email')->get(); +``` The `distinct` method allows you to force the query to return distinct results: - $users = Db::table('users')->distinct()->get(); +```php +$users = Db::table('users')->distinct()->get(); +``` If you already have a query builder instance and you wish to add a column to its existing select clause, you may use the `addSelect` method: - $query = Db::table('users')->select('name'); +```php +$query = Db::table('users')->select('name'); - $users = $query->addSelect('age')->get(); +$users = $query->addSelect('age')->get(); +``` If you wish to concatenate columns and/or strings together, you may use the `selectConcat` method to specify a list of concatenated values and the resulting alias. If you wish to use strings in the concatenation, you must provide a quoted string: - $query = Db::table('users')->selectConcat(['"Name: "', 'first_name', 'last_name'], 'name_string'); +```php +$query = Db::table('users')->selectConcat(['"Name: "', 'first_name', 'last_name'], 'name_string'); - $nameString = $query->first()->name_string; // Name: John Smith +$nameString = $query->first()->name_string; // Name: John Smith +``` #### Raw expressions Sometimes you may need to use a raw expression in a query. To create a raw expression, you may use the `Db::raw` method: - $users = Db::table('users') - ->select(Db::raw('count(*) as user_count, status')) - ->where('status', '<>', 1) - ->groupBy('status') - ->get(); +```php +$users = Db::table('users') + ->select(Db::raw('count(*) as user_count, status')) + ->where('status', '<>', 1) + ->groupBy('status') + ->get(); +``` > **NOTE:** Raw statements will be injected into the query as strings, so you should be extremely careful to not create SQL injection vulnerabilities. @@ -173,44 +206,54 @@ Instead of using `Db::raw`, you may also use the following methods to insert a r The `selectRaw` method can be used in place of `addSelect(Db::raw(...)).` This method accepts an optional array of bindings as its second argument: - $orders = Db::table('orders') - ->selectRaw('price * ? as price_with_tax', [1.0825]) - ->get(); +```php +$orders = Db::table('orders') + ->selectRaw('price * ? as price_with_tax', [1.0825]) + ->get(); +``` **whereRaw / orWhereRaw** The `whereRaw` and `orWhereRaw` methods can be used to inject a raw `where` clause into your query. These methods accept an optional array of bindings as their second argument: - $orders = Db::table('orders') - ->whereRaw('price > IF(state = "TX", ?, 100)', [200]) - ->get(); +```php +$orders = Db::table('orders') + ->whereRaw('price > IF(state = "TX", ?, 100)', [200]) + ->get(); +``` **havingRaw / orHavingRaw** The `havingRaw` and `orHavingRaw` methods may be used to set a raw string as the value of the `having` clause. These methods accept an optional array of bindings as their second argument: - $orders = Db::table('orders') - ->select('department', Db::raw('SUM(price) as total_sales')) - ->groupBy('department') - ->havingRaw('SUM(price) > ?', [2500]) - ->get(); +```php +$orders = Db::table('orders') + ->select('department', Db::raw('SUM(price) as total_sales')) + ->groupBy('department') + ->havingRaw('SUM(price) > ?', [2500]) + ->get(); +``` **orderByRaw** The `orderByRaw` method may be used to set a raw string as the value of the order by clause: - $orders = Db::table('orders') - ->orderByRaw('updated_at - created_at DESC') - ->get(); +```php +$orders = Db::table('orders') + ->orderByRaw('updated_at - created_at DESC') + ->get(); +``` **groupByRaw** The `groupByRaw` method may be used to set a raw string as the value of the group by clause: - $orders = Db::table('orders') - ->select('city', 'state') - ->groupByRaw('city, state') - ->get(); +```php +$orders = Db::table('orders') + ->select('city', 'state') + ->groupByRaw('city, state') + ->get(); +``` ## Joins @@ -219,77 +262,91 @@ The `groupByRaw` method may be used to set a raw string as the value of the grou The query builder may also be used to write join statements. To perform a basic SQL "inner join", you may use the `join` method on a query builder instance. The first argument passed to the `join` method is the name of the table you need to join to, while the remaining arguments specify the column constraints for the join. Of course, as you can see, you can join to multiple tables in a single query: - $users = Db::table('users') - ->join('contacts', 'users.id', '=', 'contacts.user_id') - ->join('orders', 'users.id', '=', 'orders.user_id') - ->select('users.*', 'contacts.phone', 'orders.price') - ->get(); +```php +$users = Db::table('users') + ->join('contacts', 'users.id', '=', 'contacts.user_id') + ->join('orders', 'users.id', '=', 'orders.user_id') + ->select('users.*', 'contacts.phone', 'orders.price') + ->get(); +``` #### Left join / right join statement If you would like to perform a "left join" or "right join" instead of an "inner join", use the `leftJoin` or `rightJoin` method. The `leftJoin` and `rightJoin` methods have the same signature as the `join` method: - $users = Db::table('users') - ->leftJoin('posts', 'users.id', '=', 'posts.user_id') - ->get(); +```php +$users = Db::table('users') + ->leftJoin('posts', 'users.id', '=', 'posts.user_id') + ->get(); - $users = Db::table('users') - ->rightJoin('posts', 'users.id', '=', 'posts.user_id') - ->get(); +$users = Db::table('users') + ->rightJoin('posts', 'users.id', '=', 'posts.user_id') + ->get(); +``` #### Cross join statement To perform a "cross join" use the `crossJoin` method with the name of the table you wish to cross join to. Cross joins generate a cartesian product between the first table and the joined table: - $users = Db::table('sizes') - ->crossJoin('colors') - ->get(); +```php +$users = Db::table('sizes') + ->crossJoin('colors') + ->get(); +``` #### Advanced join statements You may also specify more advanced join clauses. To get started, pass a `Closure` as the second argument into the `join` method. The `Closure` will receive a `JoinClause` object which allows you to specify constraints on the `join` clause: - Db::table('users') - ->join('contacts', function ($join) { - $join->on('users.id', '=', 'contacts.user_id')->orOn(...); - }) - ->get(); +```php +Db::table('users') + ->join('contacts', function ($join) { + $join->on('users.id', '=', 'contacts.user_id')->orOn(...); + }) + ->get(); +``` If you would like to use a "where" style clause on your joins, you may use the `where` and `orWhere` methods on a join. Instead of comparing two columns, these methods will compare the column against a value: - Db::table('users') - ->join('contacts', function ($join) { - $join->on('users.id', '=', 'contacts.user_id') - ->where('contacts.user_id', '>', 5); - }) - ->get(); +```php +Db::table('users') + ->join('contacts', function ($join) { + $join->on('users.id', '=', 'contacts.user_id') + ->where('contacts.user_id', '>', 5); + }) + ->get(); +``` #### Subquery joins You may use the `joinSub`, `leftJoinSub`, and `rightJoinSub` methods to join a query to a subquery. Each of these methods receive three arguments: the subquery, its table alias, and a Closure that defines the related columns: - $latestPosts = Db::table('posts') - ->select('user_id', Db::raw('MAX(created_at) as last_post_created_at')) - ->where('is_published', true) - ->groupBy('user_id'); +```php +$latestPosts = Db::table('posts') + ->select('user_id', Db::raw('MAX(created_at) as last_post_created_at')) + ->where('is_published', true) + ->groupBy('user_id'); - $users = Db::table('users') - ->joinSub($latestPosts, 'latest_posts', function ($join) { - $join->on('users.id', '=', 'latest_posts.user_id'); - })->get(); +$users = Db::table('users') + ->joinSub($latestPosts, 'latest_posts', function ($join) { + $join->on('users.id', '=', 'latest_posts.user_id'); + })->get(); +``` ## Unions The query builder also provides a quick way to "union" two queries together. For example, you may create an initial query, and then use the `union` method to union it with a second query: - $first = Db::table('users') - ->whereNull('first_name'); +```php +$first = Db::table('users') + ->whereNull('first_name'); - $users = Db::table('users') - ->whereNull('last_name') - ->union($first) - ->get(); +$users = Db::table('users') + ->whereNull('last_name') + ->union($first) + ->get(); +``` The `unionAll` method is also available and has the same method signature as `union`. @@ -302,34 +359,42 @@ To add `where` clauses to the query, use the `where` method on a query builder i For example, here is a query that verifies the value of the "votes" column is equal to 100: - $users = Db::table('users')->where('votes', '=', 100)->get(); +```php +$users = Db::table('users')->where('votes', '=', 100)->get(); +``` For convenience, if you simply want to verify that a column is equal to a given value, you may pass the value directly as the second argument to the `where` method: - $users = Db::table('users')->where('votes', 100)->get(); +```php +$users = Db::table('users')->where('votes', 100)->get(); +``` Of course, you may use a variety of other operators when writing a `where` clause: - $users = Db::table('users') - ->where('votes', '>=', 100) - ->get(); +```php +$users = Db::table('users') + ->where('votes', '>=', 100) + ->get(); - $users = Db::table('users') - ->where('votes', '<>', 100) - ->get(); +$users = Db::table('users') + ->where('votes', '<>', 100) + ->get(); - $users = Db::table('users') - ->where('name', 'like', 'T%') - ->get(); +$users = Db::table('users') + ->where('name', 'like', 'T%') + ->get(); +``` #### "Or" statements You may chain where constraints together, as well as add `or` clauses to the query. The `orWhere` method accepts the same arguments as the `where` method: - $users = Db::table('users') - ->where('votes', '>', 100) - ->orWhere('name', 'John') - ->get(); +```php +$users = Db::table('users') + ->where('votes', '>', 100) + ->orWhere('name', 'John') + ->get(); +``` > **Tip:** You can also prefix `or` to any of the where statements methods below, to make the condition an "OR" condition - for example, `orWhereBetween`, `orWhereIn`, etc. @@ -337,42 +402,54 @@ You may chain where constraints together, as well as add `or` clauses to the que The `whereBetween` method verifies that a column's value is between two values: - $users = Db::table('users') - ->whereBetween('votes', [1, 100])->get(); +```php +$users = Db::table('users') + ->whereBetween('votes', [1, 100])->get(); +``` The `whereNotBetween` method verifies that a column's value lies outside of two values: - $users = Db::table('users') - ->whereNotBetween('votes', [1, 100]) - ->get(); +```php +$users = Db::table('users') + ->whereNotBetween('votes', [1, 100]) + ->get(); +``` #### "Where in" statements The `whereIn` method verifies that a given column's value is contained within the given array: - $users = Db::table('users') - ->whereIn('id', [1, 2, 3]) - ->get(); +```php +$users = Db::table('users') + ->whereIn('id', [1, 2, 3]) + ->get(); +``` The `whereNotIn` method verifies that the given column's value is **not** contained in the given array: - $users = Db::table('users') - ->whereNotIn('id', [1, 2, 3]) - ->get(); +```php +$users = Db::table('users') + ->whereNotIn('id', [1, 2, 3]) + ->get(); +``` #### "Where null" statements The `whereNull` method verifies that the value of the given column is `NULL`: - $users = Db::table('users') - ->whereNull('updated_at') - ->get(); +```php +$users = Db::table('users') + ->whereNull('updated_at') + ->get(); +``` The `whereNotNull` method verifies that the column's value is **not** `NULL`: - $users = Db::table('users') - ->whereNotNull('updated_at') - ->get(); +```php +$users = Db::table('users') + ->whereNotNull('updated_at') + ->get(); +``` ### Advanced where clauses @@ -381,96 +458,116 @@ The `whereNotNull` method verifies that the column's value is **not** `NULL`: Sometimes you may need to create more advanced where clauses such as "where exists" or nested parameter groupings. The Laravel query builder can handle these as well. To get started, let's look at an example of grouping constraints within parenthesis: - Db::table('users') - ->where('name', '=', 'John') - ->orWhere(function ($query) { - $query->where('votes', '>', 100) - ->where('title', '<>', 'Admin'); - }) - ->get(); +```php +Db::table('users') + ->where('name', '=', 'John') + ->orWhere(function ($query) { + $query->where('votes', '>', 100) + ->where('title', '<>', 'Admin'); + }) + ->get(); +``` As you can see, passing `Closure` into the `orWhere` method instructs the query builder to begin a constraint group. The `Closure` will receive a query builder instance which you can use to set the constraints that should be contained within the parenthesis group. The example above will produce the following SQL: - select * from users where name = 'John' or (votes > 100 and title <> 'Admin') +```sql +select * from users where name = 'John' or (votes > 100 and title <> 'Admin') +``` #### Exists statements The `whereExists` method allows you to write `where exist` SQL clauses. The `whereExists` method accepts a `Closure` argument, which will receive a query builder instance allowing you to define the query that should be placed inside of the "exists" clause: - Db::table('users') - ->whereExists(function ($query) { - $query->select(Db::raw(1)) - ->from('orders') - ->whereRaw('orders.user_id = users.id'); - }) - ->get(); +```php +Db::table('users') + ->whereExists(function ($query) { + $query->select(Db::raw(1)) + ->from('orders') + ->whereRaw('orders.user_id = users.id'); + }) + ->get(); +``` The query above will produce the following SQL: - select * from users where exists ( - select 1 from orders where orders.user_id = users.id - ) +```php +select * from users where exists ( + select 1 from orders where orders.user_id = users.id +) +``` #### JSON "where" statements Winter CMS also supports querying JSON column types on databases that provide support for JSON column types. To query a JSON column, use the `->` operator: - $users = Db::table('users') - ->where('options->language', 'en') - ->get(); +```php +$users = Db::table('users') + ->where('options->language', 'en') + ->get(); - $users = Db::table('users') - ->where('preferences->dining->meal', 'salad') - ->get(); +$users = Db::table('users') + ->where('preferences->dining->meal', 'salad') + ->get(); +``` You may use `whereJsonContains` to query JSON arrays (not supported on SQLite): - $users = Db::table('users') - ->whereJsonContains('options->languages', 'en') - ->get(); +```php +$users = Db::table('users') + ->whereJsonContains('options->languages', 'en') + ->get(); +``` MySQL and PostgreSQL support `whereJsonContains` with multiple values: - $users = Db::table('users') - ->whereJsonContains('options->languages', ['en', 'de']) - ->get(); +```php +$users = Db::table('users') + ->whereJsonContains('options->languages', ['en', 'de']) + ->get(); +``` You may use `whereJsonLength` to query JSON arrays by their length: - $users = Db::table('users') - ->whereJsonLength('options->languages', 0) - ->get(); +```php +$users = Db::table('users') + ->whereJsonLength('options->languages', 0) + ->get(); - $users = Db::table('users') - ->whereJsonLength('options->languages', '>', 1) - ->get(); +$users = Db::table('users') + ->whereJsonLength('options->languages', '>', 1) + ->get(); +``` ### Conditional clauses Sometimes you may want clauses to apply to a query only when something else is true. For instance you may only want to apply a `where` statement if a given input value is present on the incoming request. You may accomplish this using the `when` method: - $role = $request->input('role'); +```php +$role = $request->input('role'); - $users = Db::table('users') - ->when($role, function ($query, $role) { - return $query->where('role_id', $role); - }) - ->get(); +$users = Db::table('users') + ->when($role, function ($query, $role) { + return $query->where('role_id', $role); + }) + ->get(); +``` The `when` method only executes the given Closure when the first parameter is `true`. If the first parameter is `false`, the Closure will not be executed. You may pass another Closure as the third parameter to the `when` method. This Closure will execute if the first parameter evaluates as false. To illustrate how this feature may be used, we will use it to configure the default sorting of a query: - $sortBy = null; +```php +$sortBy = null; - $users = Db::table('users') - ->when($sortBy, function ($query, $sortBy) { - return $query->orderBy($sortBy); - }, function ($query) { - return $query->orderBy('name'); - }) - ->get(); +$users = Db::table('users') + ->when($sortBy, function ($query, $sortBy) { + return $query->orderBy($sortBy); + }, function ($query) { + return $query->orderBy('name'); + }) + ->get(); +``` ## Ordering, grouping, limit, & offset @@ -479,41 +576,51 @@ You may pass another Closure as the third parameter to the `when` method. This C The `orderBy` method allows you to sort the result of the query by a given column. The first argument to the `orderBy` method should be the column you wish to sort by, while the second argument controls the direction of the sort and may be either `asc` or `desc`: - $users = Db::table('users') - ->orderBy('name', 'desc') - ->get(); +```php +$users = Db::table('users') + ->orderBy('name', 'desc') + ->get(); +``` #### Latest / oldest The `latest` and `oldest` methods allow you to easily order results by date. By default, result will be ordered by the `created_at` column. Or, you may pass the column name that you wish to sort by: - $user = Db::table('users') - ->latest() - ->first(); +```php +$user = Db::table('users') + ->latest() + ->first(); +``` #### Random order The `inRandomOrder` method may be used to sort the query results randomly. For example, you may use this method to fetch a random user: - $randomUser = Db::table('users') - ->inRandomOrder() - ->first(); +```php +$randomUser = Db::table('users') + ->inRandomOrder() + ->first(); +``` #### Grouping The `groupBy` and `having` methods may be used to group the query results. The `having` method's signature is similar to that of the `where` method: - $users = Db::table('users') - ->groupBy('account_id') - ->having('account_id', '>', 100) - ->get(); +```php +$users = Db::table('users') + ->groupBy('account_id') + ->having('account_id', '>', 100) + ->get(); +``` You may pass multiple arguments to the `groupBy` method to group by multiple columns: - $users = Db::table('users') - ->groupBy('first_name', 'status') - ->having('account_id', '>', 100) - ->get(); +```php +$users = Db::table('users') + ->groupBy('first_name', 'status') + ->having('account_id', '>', 100) + ->get(); +``` For more advanced `having` statements, you may wish to use the [`havingRaw`](#aggregates) method. @@ -521,31 +628,39 @@ For more advanced `having` statements, you may wish to use the [`havingRaw`](#ag To limit the number of results returned from the query, or to skip a given number of results in the query (`OFFSET`), you may use the `skip` and `take` methods: - $users = Db::table('users')->skip(10)->take(5)->get(); +```php +$users = Db::table('users')->skip(10)->take(5)->get(); +``` ## Inserts The query builder also provides an `insert` method for inserting records into the database table. The `insert` method accepts an array of column names and values to insert: - Db::table('users')->insert( - ['email' => 'john@example.com', 'votes' => 0] - ); +```php +Db::table('users')->insert( + ['email' => 'john@example.com', 'votes' => 0] +); +``` You may even insert several records into the table with a single call to `insert` by passing an array of arrays. Each array represents a row to be inserted into the table: - Db::table('users')->insert([ - ['email' => 'taylor@example.com', 'votes' => 0], - ['email' => 'dayle@example.com', 'votes' => 0] - ]); +```php +Db::table('users')->insert([ + ['email' => 'taylor@example.com', 'votes' => 0], + ['email' => 'dayle@example.com', 'votes' => 0] +]); +``` #### Auto-incrementing IDs If the table has an auto-incrementing id, use the `insertGetId` method to insert a record and then retrieve the ID: - $id = Db::table('users')->insertGetId( - ['email' => 'john@example.com', 'votes' => 0] - ); +```php +$id = Db::table('users')->insertGetId( + ['email' => 'john@example.com', 'votes' => 0] +); +``` > **NOTE:** When using the PostgreSQL database driver, the insertGetId method expects the auto-incrementing column to be named `id`. If you would like to retrieve the ID from a different "sequence", you may pass the sequence name as the second parameter to the `insertGetId` method. @@ -554,9 +669,11 @@ If the table has an auto-incrementing id, use the `insertGetId` method to insert In addition to inserting records into the database, the query builder can also update existing records using the `update` method. The `update` method, like the `insert` method, accepts an array of column and value pairs containing the columns to be updated. You may constrain the `update` query using `where` clauses: - Db::table('users') - ->where('id', 1) - ->update(['votes' => 1]); +```php +Db::table('users') + ->where('id', 1) + ->update(['votes' => 1]); +``` #### Update or Insert (One query per row) @@ -564,20 +681,24 @@ Sometimes you may want to update an existing record in the database or create it The `updateOrInsert` method will first attempt to locate a matching database record using the first argument's column and value pairs. If the record exists, it will be updated with the values in the second argument. If the record can not be found, a new record will be inserted with the merged attributes of both arguments: - Db::table('users') - ->updateOrInsert( - ['email' => 'john@example.com', 'name' => 'John'], - ['votes' => '2'] - ); +```php +Db::table('users') + ->updateOrInsert( + ['email' => 'john@example.com', 'name' => 'John'], + ['votes' => '2'] + ); +``` #### Update or Insert / `upsert()` (Batch query to process multiple rows in one DB call) The `upsert` method will insert rows that do not exist and update the rows that already exist with the new values. The method's first argument consists of the values to insert or update, while the second argument lists the column(s) that uniquely identify records within the associated table. The method's third and final argument is an array of columns that should be updated if a matching record already exists in the database: - DB::table('flights')->upsert([ - ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], - ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] - ], ['departure', 'destination'], ['price']); +```php +DB::table('flights')->upsert([ + ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], + ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] +], ['departure', 'destination'], ['price']); +``` > **NOTE:** All databases except SQL Server require the columns in the second argument of the `upsert` method to have a "primary" or "unique" index. @@ -585,9 +706,11 @@ The `upsert` method will insert rows that do not exist and update the rows that When updating a JSON column, you should use `->` syntax to access the appropriate key in the JSON object. This operation is supported on MySQL 5.7+ and PostgreSQL 9.5+: - $affected = Db::table('users') - ->where('id', 1) - ->update(['options->enabled' => true]); +```php +$affected = Db::table('users') + ->where('id', 1) + ->update(['options->enabled' => true]); +``` #### Increment / decrement @@ -595,43 +718,57 @@ The query builder also provides convenient methods for incrementing or decrement Both of these methods accept at least one argument: the column to modify. A second argument may optionally be passed to control the amount by which the column should be incremented / decremented. - Db::table('users')->increment('votes'); +```php +Db::table('users')->increment('votes'); - Db::table('users')->increment('votes', 5); +Db::table('users')->increment('votes', 5); - Db::table('users')->decrement('votes'); +Db::table('users')->decrement('votes'); - Db::table('users')->decrement('votes', 5); +Db::table('users')->decrement('votes', 5); +``` You may also specify additional columns to update during the operation: - Db::table('users')->increment('votes', 1, ['name' => 'John']); +```php +Db::table('users')->increment('votes', 1, ['name' => 'John']); +``` ## Deletes The query builder may also be used to delete records from the table via the `delete` method: - Db::table('users')->delete(); +```php +Db::table('users')->delete(); +``` You may constrain `delete` statements by adding `where` clauses before calling the `delete` method: - Db::table('users')->where('votes', '<', 100)->delete(); +```php +Db::table('users')->where('votes', '<', 100)->delete(); +``` If you wish to truncate the entire table, which will remove all rows and reset the auto-incrementing ID to zero, you may use the `truncate` method: - Db::table('users')->truncate(); +```php +Db::table('users')->truncate(); +``` ## Pessimistic locking The query builder also includes a few functions to help you do "pessimistic locking" on your `select` statements. To run the statement with a "shared lock", you may use the `sharedLock` method on a query. A shared lock prevents the selected rows from being modified until your transaction commits: - Db::table('users')->where('votes', '>', 100)->sharedLock()->get(); +```php +Db::table('users')->where('votes', '>', 100)->sharedLock()->get(); +``` Alternatively, you may use the `lockForUpdate` method. A "for update" lock prevents the rows from being modified or from being selected with another shared lock: - Db::table('users')->where('votes', '>', 100)->lockForUpdate()->get(); +```php +Db::table('users')->where('votes', '>', 100)->lockForUpdate()->get(); +``` ## Caching queries @@ -641,7 +778,9 @@ Alternatively, you may use the `lockForUpdate` method. A "for update" lock preve You may easily cache the results of a query using the [Cache service](../services/cache). Simply chain the `remember` or `rememberForever` method when preparing the query. - $users = Db::table('users')->remember(10)->get(); +```php +$users = Db::table('users')->remember(10)->get(); +``` In this example, the results of the query will be cached for ten minutes. While the results are cached, the query will not be run against the database, and the results will be loaded from the default cache driver specified for your application. @@ -650,19 +789,25 @@ In this example, the results of the query will be cached for ten minutes. While Duplicate queries across the same request can be prevented by using in-memory caching. This feature is enabled by default for [queries prepared by a model](../database/model#retrieving-models) but not those generated directly using the `Db` facade. - Db::table('users')->get(); // Result from database - Db::table('users')->get(); // Result from database +```php +Db::table('users')->get(); // Result from database +Db::table('users')->get(); // Result from database - Model::all(); // Result from database - Model::all(); // Result from in-memory cache +Model::all(); // Result from database +Model::all(); // Result from in-memory cache +``` You may enable or disable the duplicate cache with either the `enableDuplicateCache` or `disableDuplicateCache` method. - Db::table('users')->enableDuplicateCache()->get(); +```php +Db::table('users')->enableDuplicateCache()->get(); +``` If a query is stored in the cache, it will automatically be cleared when an insert, update, delete, or truncate statement is used. However you may clear the cache manually using the `flushDuplicateCache` method. - Db::flushDuplicateCache(); +```php +Db::flushDuplicateCache(); +``` > **NOTE**: In-memory caching is disabled entirely when running via the command-line interface (CLI). @@ -671,6 +816,8 @@ If a query is stored in the cache, it will automatically be cleared when an inse You may use the `dd` or `dump` methods while building a query to dump the query bindings and SQL. The `dd` method will display the debug information and then stop executing the request. The `dump` method will display the debug information but allow the request to keep executing: - Db::table('users')->where('votes', '>', 100)->dd(); +```php +Db::table('users')->where('votes', '>', 100)->dd(); - Db::table('users')->where('votes', '>', 100)->dump(); +Db::table('users')->where('votes', '>', 100)->dump(); +``` diff --git a/database-relations.md b/database-relations.md index 24aad8c5..c85984b4 100644 --- a/database-relations.md +++ b/database-relations.md @@ -40,20 +40,26 @@ Database tables are often related to one another. For example, a blog post may h Model relationships are defined as properties on your model classes. An example of defining relationships: - class User extends Model - { - public $hasMany = [ - 'posts' => 'Acme\Blog\Models\Post' - ] - } +```php +class User extends Model +{ + public $hasMany = [ + 'posts' => 'Acme\Blog\Models\Post' + ] +} +``` Relationships like models themselves, also serve as powerful [query builders](query), accessing relationships as functions provides powerful method chaining and querying capabilities. For example: - $user->posts()->where('is_active', true)->get(); +```php +$user->posts()->where('is_active', true)->get(); +``` Accessing a relationship as a property is also possible: - $user->posts; +```php +$user->posts; +``` > **NOTE**: All relationship queries have [in-memory caching enabled](../database/query#in-memory-caching) by default. The `load($relation)` method won't force cache to flush. To reload the memory cache use the `reloadRelations()` or the `reload()` methods on the model object. @@ -62,9 +68,11 @@ Accessing a relationship as a property is also possible: Each definition can be an array where the key is the relation name and the value is a detail array. The detail array's first value is always the related model class name and all other values are parameters that must have a key name. - public $hasMany = [ - 'posts' => ['Acme\Blog\Models\Post', 'delete' => true] - ]; +```php +public $hasMany = [ + 'posts' => ['Acme\Blog\Models\Post', 'delete' => true] +]; +``` The following are parameters that can be used with all relations: @@ -80,40 +88,46 @@ Argument | Description Example filter using **order** and **conditions**: +```php +public $belongsToMany = [ + 'categories' => [ + 'Acme\Blog\Models\Category', + 'order' => 'name desc', + 'conditions' => 'is_active = 1' + ] +]; +``` + +Example filter using **scope**: + +```php +class Post extends Model +{ public $belongsToMany = [ 'categories' => [ 'Acme\Blog\Models\Category', - 'order' => 'name desc', - 'conditions' => 'is_active = 1' + 'scope' => 'isActive' ] ]; +} -Example filter using **scope**: - - class Post extends Model +class Category extends Model +{ + public function scopeIsActive($query) { - public $belongsToMany = [ - 'categories' => [ - 'Acme\Blog\Models\Category', - 'scope' => 'isActive' - ] - ]; - } - - class Category extends Model - { - public function scopeIsActive($query) - { - return $query->where('is_active', true)->orderBy('name', 'desc'); - } + return $query->where('is_active', true)->orderBy('name', 'desc'); } +} +``` Example filter using **count**: - public $belongsToMany = [ - 'users' => ['Backend\Models\User'], - 'users_count' => ['Backend\Models\User', 'count' => true] - ]; +```php +public $belongsToMany = [ + 'users' => ['Backend\Models\User'], + 'users_count' => ['Backend\Models\User', 'count' => true] +]; +``` ## Relationship types @@ -132,133 +146,167 @@ The following relations types are available: A one-to-one relationship is a very basic relation. For example, a `User` model might be associated with one `Phone`. To define this relationship, we add a `phone` entry to the `$hasOne` property on the `User` model. - 'Acme\Blog\Models\Phone' - ]; - } +class User extends Model +{ + public $hasOne = [ + 'phone' => 'Acme\Blog\Models\Phone' + ]; +} +``` Once the relationship is defined, we may retrieve the related record using the model property of the same name. These properties are dynamic and allow you to access them as if they were regular attributes on the model: - $phone = User::find(1)->phone; +```php +$phone = User::find(1)->phone; +``` The model assumes the foreign key of the relationship based on the model name. In this case, the `Phone` model is automatically assumed to have a `user_id` foreign key. If you wish to override this convention, you may pass the `key` parameter to the definition: - public $hasOne = [ - 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id'] - ]; +```php +public $hasOne = [ + 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id'] +]; +``` Additionally, the model assumes that the foreign key should have a value matching the `id` column of the parent. In other words, it will look for the value of the user's `id` column in the `user_id` column of the `Phone` record. If you would like the relationship to use a value other than `id`, you may pass the `otherKey` parameter to the definition: - public $hasOne = [ - 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id', 'otherKey' => 'my_id'] - ]; +```php +public $hasOne = [ + 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id', 'otherKey' => 'my_id'] +]; +``` #### Defining the inverse of the relation Now that we can access the `Phone` model from our `User`. Let's do the opposite and define a relationship on the `Phone` model that will let us access the `User` that owns the phone. We can define the inverse of a `hasOne` relationship using the `$belongsTo` property: - class Phone extends Model - { - public $belongsTo = [ - 'user' => 'Acme\Blog\Models\User' - ]; - } +```php +class Phone extends Model +{ + public $belongsTo = [ + 'user' => 'Acme\Blog\Models\User' + ]; +} +``` In the example above, the model will try to match the `user_id` from the `Phone` model to an `id` on the `User` model. It determines the default foreign key name by examining the name of the relationship definition and suffixing the name with `_id`. However, if the foreign key on the `Phone` model is not `user_id`, you may pass a custom key name using the `key` parameter on the definition: - public $belongsTo = [ - 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id'] - ]; +```php +public $belongsTo = [ + 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id'] +]; +``` If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass the `otherKey` parameter to the definition specifying your parent table's custom key: - public $belongsTo = [ - 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id', 'otherKey' => 'my_id'] - ]; +```php +public $belongsTo = [ + 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id', 'otherKey' => 'my_id'] +]; +``` #### Default models The `belongsTo` relationship lets you define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `Acme\Blog\Models\User` model if no `user` is attached to the post: - public $belongsTo = [ - 'user' => ['Acme\Blog\Models\User', 'default' => true] - ]; +```php +public $belongsTo = [ + 'user' => ['Acme\Blog\Models\User', 'default' => true] +]; +``` To populate the default model with attributes, you may pass an array to the `default` parameter: - public $belongsTo = [ - 'user' => [ - 'Acme\Blog\Models\User', - 'default' => ['name' => 'Guest'] - ] - ]; +```php +public $belongsTo = [ + 'user' => [ + 'Acme\Blog\Models\User', + 'default' => ['name' => 'Guest'] + ] +]; +``` ### One To Many A one-to-many relationship is used to define relationships where a single model owns any amount of other models. For example, a blog post may have an infinite number of comments. Like all other relationships, one-to-many relationships are defined adding an entry to the `$hasMany` property on your model: - class Post extends Model - { - public $hasMany = [ - 'comments' => 'Acme\Blog\Models\Comment' - ]; - } +```php +class Post extends Model +{ + public $hasMany = [ + 'comments' => 'Acme\Blog\Models\Comment' + ]; +} +``` Remember, the model will automatically determine the proper foreign key column on the `Comment` model. By convention, it will take the "snake case" name of the owning model and suffix it with `_id`. So for this example, we can assume the foreign key on the `Comment` model is `post_id`. Once the relationship has been defined, we can access the collection of comments by accessing the `comments` property. Remember, since the model provides "dynamic properties", we can access relationships as if they were defined as properties on the model: - $comments = Post::find(1)->comments; +```php +$comments = Post::find(1)->comments; - foreach ($comments as $comment) { - // - } +foreach ($comments as $comment) { + // +} +``` Of course, since all relationships also serve as query builders, you can add further constraints to which comments are retrieved by calling the `comments` method and continuing to chain conditions onto the query: - $comments = Post::find(1)->comments()->where('title', 'foo')->first(); +```php +$comments = Post::find(1)->comments()->where('title', 'foo')->first(); +``` Like the `hasOne` relation, you may also override the foreign and local keys by passing the `key` and `otherKey` parameters on the definition respectively: - public $hasMany = [ - 'comments' => ['Acme\Blog\Models\Comment', 'key' => 'my_post_id', 'otherKey' => 'my_id'] - ]; +```php +public $hasMany = [ + 'comments' => ['Acme\Blog\Models\Comment', 'key' => 'my_post_id', 'otherKey' => 'my_id'] +]; +``` #### Defining the inverse of the relation Now that we can access all of a post's comments, let's define a relationship to allow a comment to access its parent post. To define the inverse of a `hasMany` relationship, define the `$belongsTo` property on the child model: - class Comment extends Model - { - public $belongsTo = [ - 'post' => 'Acme\Blog\Models\Post' - ]; - } +```php +class Comment extends Model +{ + public $belongsTo = [ + 'post' => 'Acme\Blog\Models\Post' + ]; +} +``` Once the relationship has been defined, we can retrieve the `Post` model for a `Comment` by accessing the `post` "dynamic property": - $comment = Comment::find(1); +```php +$comment = Comment::find(1); - echo $comment->post->title; +echo $comment->post->title; +``` In the example above, the model will try to match the `post_id` from the `Comment` model to an `id` on the `Post` model. It determines the default foreign key name by examining the name of the relationship and suffixing it with `_id`. However, if the foreign key on the `Comment` model is not `post_id`, you may pass a custom key name using the `key` parameter: - public $belongsTo = [ - 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id'] - ]; +```php +public $belongsTo = [ + 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id'] +]; +``` If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass the `otherKey` parameter to the definition specifying your parent table's custom key: - public $belongsTo = [ - 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id', 'otherKey' => 'my_id'] - ]; +```php +public $belongsTo = [ + 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id', 'otherKey' => 'my_id'] +]; +``` ### Many To Many @@ -267,61 +315,75 @@ Many-to-many relations are slightly more complicated than `hasOne` and `hasMany` Below is an example that shows the [database table structure](../plugin/updates#migration-files) used to create the join table. - Schema::create('role_user', function($table) - { - $table->integer('user_id')->unsigned(); - $table->integer('role_id')->unsigned(); - $table->primary(['user_id', 'role_id']); - }); +```php +Schema::create('role_user', function($table) +{ + $table->integer('user_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->primary(['user_id', 'role_id']); +}); +``` Many-to-many relationships are defined adding an entry to the `$belongsToMany` property on your model class. For example, let's define the `roles` method on our `User` model: - class User extends Model - { - public $belongsToMany = [ - 'roles' => 'Acme\Blog\Models\Role' - ]; - } +```php +class User extends Model +{ + public $belongsToMany = [ + 'roles' => 'Acme\Blog\Models\Role' + ]; +} +``` Once the relationship is defined, you may access the user's roles using the `roles` dynamic property: - $user = User::find(1); +```php +$user = User::find(1); - foreach ($user->roles as $role) { - // - } +foreach ($user->roles as $role) { + // +} +``` Of course, like all other relationship types, you may call the `roles` method to continue chaining query constraints onto the relationship: - $roles = User::find(1)->roles()->orderBy('name')->get(); +```php +$roles = User::find(1)->roles()->orderBy('name')->get(); +``` As mentioned previously, to determine the table name of the relationship's joining table, the model will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing the `table` parameter to the `belongsToMany` definition: - public $belongsToMany = [ - 'roles' => ['Acme\Blog\Models\Role', 'table' => 'acme_blog_role_user'] - ]; +```php +public $belongsToMany = [ + 'roles' => ['Acme\Blog\Models\Role', 'table' => 'acme_blog_role_user'] +]; +``` In addition to customizing the name of the joining table, you may also customize the column names of the keys on the table by passing additional parameters to the `belongsToMany` definition. The `key` parameter is the foreign key name of the model on which you are defining the relationship, while the `otherKey` parameter is the foreign key name of the model that you are joining to: - public $belongsToMany = [ - 'roles' => [ - 'Acme\Blog\Models\Role', - 'table' => 'acme_blog_role_user', - 'key' => 'my_user_id', - 'otherKey' => 'my_role_id' - ] - ]; +```php +public $belongsToMany = [ + 'roles' => [ + 'Acme\Blog\Models\Role', + 'table' => 'acme_blog_role_user', + 'key' => 'my_user_id', + 'otherKey' => 'my_role_id' + ] +]; +``` #### Defining the inverse of the relationship To define the inverse of a many-to-many relationship, you simply place another `$belongsToMany` property on your related model. To continue our user roles example, let's define the `users` relationship on the `Role` model: - class Role extends Model - { - public $belongsToMany = [ - 'users' => 'Acme\Blog\Models\User' - ]; - } +```php +class Role extends Model +{ + public $belongsToMany = [ + 'users' => 'Acme\Blog\Models\User' + ]; +} +``` As you can see, the relationship is defined exactly the same as its `User` counterpart, with the exception of simply referencing the `Acme\Blog\Models\User` model. Since we're reusing the `$belongsToMany` property, all of the usual table and key customization options are available when defining the inverse of many-to-many relationships. @@ -329,28 +391,34 @@ As you can see, the relationship is defined exactly the same as its `User` count As you have already learned, working with many-to-many relations requires the presence of an intermediate join table. Models provide some very helpful ways of interacting with this table. For example, let's assume our `User` object has many `Role` objects that it is related to. After accessing this relationship, we may access the intermediate table using the `pivot` attribute on the models: - $user = User::find(1); +```php +$user = User::find(1); - foreach ($user->roles as $role) { - echo $role->pivot->created_at; - } +foreach ($user->roles as $role) { + echo $role->pivot->created_at; +} +``` Notice that each `Role` model we retrieve is automatically assigned a `pivot` attribute. This attribute contains a model representing the intermediate table, and may be used like any other model. By default, only the model keys will be present on the `pivot` object. If your pivot table contains extra attributes, you must specify them when defining the relationship: - public $belongsToMany = [ - 'roles' => [ - 'Acme\Blog\Models\Role', - 'pivot' => ['column1', 'column2'] - ] - ]; +```php +public $belongsToMany = [ + 'roles' => [ + 'Acme\Blog\Models\Role', + 'pivot' => ['column1', 'column2'] + ] +]; +``` If you want your pivot table to have automatically maintained `created_at` and `updated_at` timestamps, use the `timestamps` parameter on the relationship definition: - public $belongsToMany = [ - 'roles' => ['Acme\Blog\Models\Role', 'timestamps' => true] - ]; +```php +public $belongsToMany = [ + 'roles' => ['Acme\Blog\Models\Role', 'timestamps' => true] +]; +``` These are the parameters supported for `belongsToMany` relations: @@ -388,29 +456,33 @@ Though `posts` does not contain a `country_id` column, the `hasManyThrough` rela Now that we have examined the table structure for the relationship, let's define it on the `Country` model: - class Country extends Model - { - public $hasManyThrough = [ - 'posts' => [ - 'Acme\Blog\Models\Post', - 'through' => 'Acme\Blog\Models\User' - ], - ]; - } - -The first argument passed to the `$hasManyThrough` relation is the name of the final model we wish to access, while the `through` parameter is the name of the intermediate model. - -Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. - +```php +class Country extends Model +{ public $hasManyThrough = [ 'posts' => [ 'Acme\Blog\Models\Post', - 'key' => 'my_country_id', - 'through' => 'Acme\Blog\Models\User', - 'throughKey' => 'my_user_id', - 'otherKey' => 'my_id' + 'through' => 'Acme\Blog\Models\User' ], ]; +} +``` + +The first argument passed to the `$hasManyThrough` relation is the name of the final model we wish to access, while the `through` parameter is the name of the intermediate model. + +Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. + +```php +public $hasManyThrough = [ + 'posts' => [ + 'Acme\Blog\Models\Post', + 'key' => 'my_country_id', + 'through' => 'Acme\Blog\Models\User', + 'throughKey' => 'my_user_id', + 'otherKey' => 'my_id' + ], +]; +``` ### Has One Through @@ -430,30 +502,33 @@ The has-one-through relationship links models through a single intermediate rela Though the `history` table does not contain a `supplier_id` column, the `hasOneThrough` relation can provide access to the user's history to the supplier model. Now that we have examined the table structure for the relationship, let's define it on the `Supplier` model: - class Supplier extends Model - { - public $hasOneThrough = [ - 'userHistory' => [ - 'Acme\Supplies\Model\History', - 'through' => 'Acme\Supplies\Model\User' - ], - ]; - } - -The first array parameter passed to the `$hasOneThrough` property is the name of the final model we wish to access, while the `through` key is the name of the intermediate model. - -Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. - +```php +class Supplier extends Model +{ public $hasOneThrough = [ 'userHistory' => [ 'Acme\Supplies\Model\History', - 'key' => 'supplier_id', 'through' => 'Acme\Supplies\Model\User' - 'throughKey' => 'user_id', - 'otherKey' => 'id' ], ]; +} +``` + +The first array parameter passed to the `$hasOneThrough` property is the name of the final model we wish to access, while the `through` key is the name of the intermediate model. + +Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. +```php +public $hasOneThrough = [ + 'userHistory' => [ + 'Acme\Supplies\Model\History', + 'key' => 'supplier_id', + 'through' => 'Acme\Supplies\Model\User' + 'throughKey' => 'user_id', + 'otherKey' => 'id' + ], +]; +``` ### Polymorphic relations @@ -487,40 +562,46 @@ Two important columns to note are the `imageable_id` and `imageable_type` column Next, let's examine the model definitions needed to build this relationship: - class Photo extends Model - { - public $morphTo = [ - 'imageable' => [] - ]; - } +```php +class Photo extends Model +{ + public $morphTo = [ + 'imageable' => [] + ]; +} - class Staff extends Model - { - public $morphOne = [ - 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] - ]; - } +class Staff extends Model +{ + public $morphOne = [ + 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] + ]; +} - class Product extends Model - { - public $morphOne = [ - 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] - ]; - } +class Product extends Model +{ + public $morphOne = [ + 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] + ]; +} +``` #### Retrieving Polymorphic relations Once your database table and models are defined, you may access the relationships via your models. For example, to access the photo for a staff member, we can simply use the `photo` dynamic property: - $staff = Staff::find(1); +```php +$staff = Staff::find(1); - $photo = $staff->photo +$photo = $staff->photo; +``` You may also retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the `morphTo` relationship. In our case, that is the `imageable` definition on the `Photo` model. So, we will access it as a dynamic property: - $photo = Photo::find(1); +```php +$photo = Photo::find(1); - $imageable = $photo->imageable; +$imageable = $photo->imageable; +``` The `imageable` relation on the `Photo` model will return either a `Staff` or `Product` instance, depending on which type of model owns the photo. @@ -551,52 +632,60 @@ A one-to-many polymorphic relation is similar to a simple one-to-many relation; Next, let's examine the model definitions needed to build this relationship: - class Comment extends Model - { - public $morphTo = [ - 'commentable' => [] - ]; - } +```php +class Comment extends Model +{ + public $morphTo = [ + 'commentable' => [] + ]; +} - class Post extends Model - { - public $morphMany = [ - 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] - ]; - } +class Post extends Model +{ + public $morphMany = [ + 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] + ]; +} - class Product extends Model - { - public $morphMany = [ - 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] - ]; - } +class Product extends Model +{ + public $morphMany = [ + 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] + ]; +} +``` #### Retrieving The Relationship Once your database table and models are defined, you may access the relationships via your models. For example, to access all of the comments for a post, we can use the `comments` dynamic property: - $post = Author\Plugin\Models\Post::find(1); +```php +$post = Author\Plugin\Models\Post::find(1); - foreach ($post->comments as $comment) { - // - } +foreach ($post->comments as $comment) { + // +} +``` You may also retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the `morphTo` relationship. In our case, that is the `commentable` definition on the `Comment` model. So, we will access it as a dynamic property: - $comment = Author\Plugin\Models\Comment::find(1); +```php +$comment = Author\Plugin\Models\Comment::find(1); - $commentable = $comment->commentable; +$commentable = $comment->commentable; +``` The `commentable` relation on the `Comment` model will return either a `Post` or `Video` instance, depending on which type of model owns the comment. You are also able to update the owner of the related model by setting the attribute with the name of the `morphTo` relationship, in this case `commentable`. - $comment = Author\Plugin\Models\Comment::find(1); - $video = Author\Plugin\Models\Video::find(1); +```php +$comment = Author\Plugin\Models\Comment::find(1); +$video = Author\Plugin\Models\Video::find(1); - $comment->commentable = $video; - $comment->save() +$comment->commentable = $video; +$comment->save(); +``` ### Many To Many @@ -626,42 +715,50 @@ In addition to "one-to-one" and "one-to-many" relations, you may also define "ma Next, we're ready to define the relationships on the model. The `Post` and `Video` models will both have a `tags` relation defined in the `$morphToMany` property on the base model class: - class Post extends Model - { - public $morphToMany = [ - 'tags' => ['Acme\Blog\Models\Tag', 'name' => 'taggable'] - ]; - } +```php +class Post extends Model +{ + public $morphToMany = [ + 'tags' => ['Acme\Blog\Models\Tag', 'name' => 'taggable'] + ]; +} +``` #### Defining the inverse of the relationship Next, on the `Tag` model, you should define a relation for each of its related models. So, for this example, we will define a `posts` relation and a `videos` relation: - class Tag extends Model - { - public $morphedByMany = [ - 'posts' => ['Acme\Blog\Models\Post', 'name' => 'taggable'], - 'videos' => ['Acme\Blog\Models\Video', 'name' => 'taggable'] - ]; - } +```php +class Tag extends Model +{ + public $morphedByMany = [ + 'posts' => ['Acme\Blog\Models\Post', 'name' => 'taggable'], + 'videos' => ['Acme\Blog\Models\Video', 'name' => 'taggable'] + ]; +} +``` #### Retrieving the relationship Once your database table and models are defined, you may access the relationships via your models. For example, to access all of the tags for a post, you can simply use the `tags` dynamic property: - $post = Post::find(1); +```php +$post = Post::find(1); - foreach ($post->tags as $tag) { - // - } +foreach ($post->tags as $tag) { + // +} +``` You may also retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the relationship defined in the `$morphedByMany` property. In our case, that is the `posts` or `videos` methods on the `Tag` model. So, you will access those relations as dynamic properties: - $tag = Tag::find(1); +```php +$tag = Tag::find(1); - foreach ($tag->videos as $video) { - // - } +foreach ($tag->videos as $video) { + // +} +``` #### Custom Polymorphic types @@ -670,12 +767,14 @@ By default, the fully qualified class name is used to store the related model ty Using a custom polymorphic type lets you decouple your database from your application's internal structure. You may define a relationship "morph map" to provide a custom name for each model instead of the class name: - use Winter\Storm\Database\Relations\Relation; +```php +use Winter\Storm\Database\Relations\Relation; - Relation::morphMap([ - 'staff' => 'Acme\Blog\Models\Staff', - 'product' => 'Acme\Blog\Models\Product', - ]); +Relation::morphMap([ + 'staff' => 'Acme\Blog\Models\Staff', + 'product' => 'Acme\Blog\Models\Product', +]); +``` The most common place to register the `morphMap` in the `boot` method of a [Plugin registration file](../plugin/registration#registration-methods). @@ -686,34 +785,40 @@ Since all types of Model relationships can be called via functions, you may call For example, imagine a blog system in which a `User` model has many associated `Post` models: - class User extends Model - { - public $hasMany = [ - 'posts' => ['Acme\Blog\Models\Post'] - ]; - } +```php +class User extends Model +{ + public $hasMany = [ + 'posts' => ['Acme\Blog\Models\Post'] + ]; +} +``` ### Access via relationship method You may query the **posts** relationship and add additional constraints to the relationship using the `posts` method. This gives you the ability to chain any of the [query builder](query) methods on the relationship. - $user = User::find(1); +```php +$user = User::find(1); - $posts = $user->posts()->where('is_active', 1)->get(); +$posts = $user->posts()->where('is_active', 1)->get(); - $post = $user->posts()->first(); +$post = $user->posts()->first(); +``` ### Access via dynamic property If you do not need to add additional constraints to a relationship query, you may simply access the relationship as if it were a property. For example, continuing to use our `User` and `Post` example models, we may access all of a user's posts using the `$user->posts` property instead. - $user = User::find(1); +```php +$user = User::find(1); - foreach ($user->posts as $post) { - // ... - } +foreach ($user->posts as $post) { + // ... +} +``` Dynamic properties are "lazy loading", meaning they will only load their relationship data when you actually access them. Because of this, developers often use [eager loading](#eager-loading) to pre-load relationships they know will be accessed after loading the model. Eager loading provides a significant reduction in SQL queries that must be executed to load a model's relations. @@ -722,111 +827,139 @@ Dynamic properties are "lazy loading", meaning they will only load their relatio When accessing the records for a model, you may wish to limit your results based on the existence of a relationship. For example, imagine you want to retrieve all blog posts that have at least one comment. To do so, you may pass the name of the relationship to the `has` method: - // Retrieve all posts that have at least one comment... - $posts = Post::has('comments')->get(); +```php +// Retrieve all posts that have at least one comment... +$posts = Post::has('comments')->get(); +``` You may also specify an operator and count to further customize the query: - // Retrieve all posts that have three or more comments... - $posts = Post::has('comments', '>=', 3)->get(); +```php +// Retrieve all posts that have three or more comments... +$posts = Post::has('comments', '>=', 3)->get(); +``` Nested `has` statements may also be constructed using "dot" notation. For example, you may retrieve all posts that have at least one comment and vote: - // Retrieve all posts that have at least one comment with votes... - $posts = Post::has('comments.votes')->get(); +```php +// Retrieve all posts that have at least one comment with votes... +$posts = Post::has('comments.votes')->get(); +``` If you need even more power, you may use the `whereHas` and `orWhereHas` methods to put "where" conditions on your `has` queries. These methods allow you to add customized constraints to a relationship constraint, such as checking the content of a comment: - // Retrieve all posts with at least one comment containing words like foo% - $posts = Post::whereHas('comments', function ($query) { - $query->where('content', 'like', 'foo%'); - })->get(); +```php +// Retrieve all posts with at least one comment containing words like foo% +$posts = Post::whereHas('comments', function ($query) { + $query->where('content', 'like', 'foo%'); +})->get(); +``` ## Eager loading When accessing relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, models can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem. To illustrate the N + 1 query problem, consider a `Book` model that is related to `Author`: - class Book extends Model - { - public $belongsTo = [ - 'author' => ['Acme\Blog\Models\Author'] - ]; - } +```php +class Book extends Model +{ + public $belongsTo = [ + 'author' => ['Acme\Blog\Models\Author'] + ]; +} +``` Now let's retrieve all books and their authors: - $books = Book::all(); +```php +$books = Book::all(); - foreach ($books as $book) { - echo $book->author->name; - } +foreach ($books as $book) { + echo $book->author->name; +} +``` This loop will execute 1 query to retrieve all of the books on the table, then another query for each book to retrieve the author. So, if we have 25 books, this loop would run 26 queries: 1 for the original book, and 25 additional queries to retrieve the author of each book. Thankfully we can use eager loading to reduce this operation to just 2 queries. When querying, you may specify which relationships should be eager loaded using the `with` method: - $books = Book::with('author')->get(); +```php +$books = Book::with('author')->get(); - foreach ($books as $book) { - echo $book->author->name; - } +foreach ($books as $book) { + echo $book->author->name; +} +``` For this operation only two queries will be executed: - select * from books +```none +select * from books - select * from authors where id in (1, 2, 3, 4, 5, ...) +select * from authors where id in (1, 2, 3, 4, 5, ...) +``` #### Eager loading multiple relationships Sometimes you may need to eager load several different relationships in a single operation. To do so, just pass additional arguments to the `with` method: - $books = Book::with('author', 'publisher')->get(); +```php +$books = Book::with('author', 'publisher')->get(); +``` #### Nested eager loading To eager load nested relationships, you may use "dot" syntax. For example, let's eager load all of the book's authors and all of the author's personal contacts in one statement: - $books = Book::with('author.contacts')->get(); +```php +$books = Book::with('author.contacts')->get(); +``` ### Constraining eager loads Sometimes you may wish to eager load a relationship, but also specify additional query constraints for the eager loading query. Here's an example: - $users = User::with([ - 'posts' => function ($query) { - $query->where('title', 'like', '%first%'); - } - ])->get(); +```php +$users = User::with([ + 'posts' => function ($query) { + $query->where('title', 'like', '%first%'); + } +])->get(); +``` In this example, the model will only eager load posts if the post's `title` column contains the word `first`. Of course, you may call other [query builder](query) methods to further customize the eager loading operation: - $users = User::with([ - 'posts' => function ($query) { - $query->orderBy('created_at', 'desc'); - } - ])->get(); +```php +$users = User::with([ + 'posts' => function ($query) { + $query->orderBy('created_at', 'desc'); + } +])->get(); +``` ### Lazy eager loading Sometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models: - $books = Book::all(); +```php +$books = Book::all(); - if ($someCondition) { - $books->load('author', 'publisher'); - } +if ($someCondition) { + $books->load('author', 'publisher'); +} +``` If you need to set additional query constraints on the eager loading query, you may pass a `Closure` to the `load` method: - $books->load([ - 'author' => function ($query) { - $query->orderBy('published_date', 'asc'); - } - ]); +```php +$books->load([ + 'author' => function ($query) { + $query->orderBy('published_date', 'asc'); + } +]); +``` ## Inserting related models @@ -842,28 +975,34 @@ Winter provides convenient methods for adding new models to relationships. Prima Use the `add` method to associate a new relationship. - $comment = new Comment(['message' => 'A new comment.']); +```php +$comment = new Comment(['message' => 'A new comment.']); - $post = Post::find(1); +$post = Post::find(1); - $comment = $post->comments()->add($comment); +$comment = $post->comments()->add($comment); +``` Notice that we did not access the `comments` relationship as a dynamic property. Instead, we called the `comments` method to obtain an instance of the relationship. The `add` method will automatically add the appropriate `post_id` value to the new `Comment` model. If you need to save multiple related models, you may use the `addMany` method: - $post = Post::find(1); +```php +$post = Post::find(1); - $post->comments()->addMany([ - new Comment(['message' => 'A new comment.']), - new Comment(['message' => 'Another comment.']), - ]); +$post->comments()->addMany([ + new Comment(['message' => 'A new comment.']), + new Comment(['message' => 'Another comment.']), +]); +``` #### Remove method Comparatively, the `remove` method can be used to disassociate a relationship, making it an orphaned record. - $post->comments()->remove($comment); +```php +$post->comments()->remove($comment); +``` In the case of many-to-many relations, the record is removed from the relationship's collection instead. @@ -871,31 +1010,39 @@ In the case of many-to-many relations, the record is removed from the relationsh In the case of a "belongs to" relationship, you may use the `dissociate` method, which doesn't require the related model passed to it. - $post->author()->dissociate(); +```php +$post->author()->dissociate(); +``` #### Adding with pivot data When working with a many-to-many relationship, the `add` method accepts an array of additional intermediate "pivot" table attributes as its second argument as an array. - $user = User::find(1); +```php +$user = User::find(1); - $pivotData = ['expires' => $expires]; +$pivotData = ['expires' => $expires]; - $user->roles()->add($role, $pivotData); +$user->roles()->add($role, $pivotData); +``` The second argument of the `add` method can also specify the session key used by [deferred binding](#deferred-binding) when passed as a string. In these cases the pivot data can be provided as the third argument instead. - $user->roles()->add($role, $sessionKey, $pivotData); +```php +$user->roles()->add($role, $sessionKey, $pivotData); +``` #### Create method While `add` and `addMany` accept a full model instance, you may also use the `create` method, that accepts a PHP array of attributes, creates a model, and inserts it into the database. - $post = Post::find(1); +```php +$post = Post::find(1); - $comment = $post->comments()->create([ - 'message' => 'A new comment.', - ]); +$comment = $post->comments()->create([ + 'message' => 'A new comment.', +]); +``` Before using the `create` method, be sure to review the documentation on attribute [mass assignment](model#mass-assignment) as the attributes in the PHP array are restricted by the model's "fillable" definition. @@ -904,39 +1051,47 @@ Before using the `create` method, be sure to review the documentation on attribu Relationships can be set directly via their properties in the same way you would access them. Setting a relationship using this approach will overwrite any relationship that existed previously. The model should be saved afterwards like you would with any attribute. - $post->author = $author; +```php +$post->author = $author; - $post->comments = [$comment1, $comment2]; +$post->comments = [$comment1, $comment2]; - $post->save(); +$post->save(); +``` Alternatively you may set the relationship using the primary key, this is useful when working with HTML forms. - // Assign to author with ID of 3 - $post->author = 3; +```php +// Assign to author with ID of 3 +$post->author = 3; - // Assign comments with IDs of 1, 2 and 3 - $post->comments = [1, 2, 3]; +// Assign comments with IDs of 1, 2 and 3 +$post->comments = [1, 2, 3]; - $post->save(); +$post->save(); +``` Relationships can be disassociated by assigning the NULL value to the property. - $post->author = null; +```php +$post->author = null; - $post->comments = null; +$post->comments = null; - $post->save(); +$post->save(); +``` Similar to [deferred binding](#deferred-binding), relationships defined on non-existent models are deferred in memory until they are saved. In this example the post does not exist yet, so the `post_id` attribute cannot be set on the comment via `$post->comments`. Therefore the association is deferred until the post is created by calling the `save` method. - $comment = Comment::find(1); +```php +$comment = Comment::find(1); - $post = new Post; +$post = new Post; - $post->comments = [$comment]; +$post->comments = [$comment]; - $post->save(); +$post->save(); +``` ### Many To Many relations @@ -945,67 +1100,83 @@ Similar to [deferred binding](#deferred-binding), relationships defined on non-e When working with many-to-many relationships, Models provide a few additional helper methods to make working with related models more convenient. For example, let's imagine a user can have many roles and a role can have many users. To attach a role to a user by inserting a record in the intermediate table that joins the models, use the `attach` method: - $user = User::find(1); +```php +$user = User::find(1); - $user->roles()->attach($roleId); +$user->roles()->attach($roleId); +``` When attaching a relationship to a model, you may also pass an array of additional data to be inserted into the intermediate table: - $user->roles()->attach($roleId, ['expires' => $expires]); +```php +$user->roles()->attach($roleId, ['expires' => $expires]); +``` Of course, sometimes it may be necessary to remove a role from a user. To remove a many-to-many relationship record, use the `detach` method. The `detach` method will remove the appropriate record out of the intermediate table; however, both models will remain in the database: - // Detach a single role from the user... - $user->roles()->detach($roleId); +```php +// Detach a single role from the user... +$user->roles()->detach($roleId); - // Detach all roles from the user... - $user->roles()->detach(); +// Detach all roles from the user... +$user->roles()->detach(); +``` For convenience, `attach` and `detach` also accept arrays of IDs as input: - $user = User::find(1); +```php +$user = User::find(1); - $user->roles()->detach([1, 2, 3]); +$user->roles()->detach([1, 2, 3]); - $user->roles()->attach([1 => ['expires' => $expires], 2, 3]); +$user->roles()->attach([1 => ['expires' => $expires], 2, 3]); +``` #### Syncing For convenience You may also use the `sync` method to construct many-to-many associations. The `sync` method accepts an array of IDs to place on the intermediate table. Any IDs that are not in the given array will be removed from the intermediate table. So, after this operation is complete, only the IDs in the array will exist in the intermediate table: - $user->roles()->sync([1, 2, 3]); +```php +$user->roles()->sync([1, 2, 3]); +``` You may also pass additional intermediate table values with the IDs: - $user->roles()->sync([1 => ['expires' => true], 2, 3]); +```php +$user->roles()->sync([1 => ['expires' => true], 2, 3]); +``` ### Touching parent timestamps When a model `belongsTo` or `belongsToMany` another model, such as a `Comment` which belongs to a `Post`, it is sometimes helpful to update the parent's timestamp when the child model is updated. For example, when a `Comment` model is updated, you may want to automatically "touch" the `updated_at` timestamp of the owning `Post`. Just add a `touches` property containing the names of the relationships to the child model: - class Comment extends Model - { - /** - * All of the relationships to be touched. - */ - protected $touches = ['post']; - - /** - * Relations - */ - public $belongsTo = [ - 'post' => ['Acme\Blog\Models\Post'] - ]; - } +```php +class Comment extends Model +{ + /** + * All of the relationships to be touched. + */ + protected $touches = ['post']; + + /** + * Relations + */ + public $belongsTo = [ + 'post' => ['Acme\Blog\Models\Post'] + ]; +} +``` Now, when you update a `Comment`, the owning `Post` will have its `updated_at` column updated as well: - $comment = Comment::find(1); +```php +$comment = Comment::find(1); - $comment->text = 'Edit to this comment!'; +$comment->text = 'Edit to this comment!'; - $comment->save(); +$comment->save(); +``` ## Deferred binding @@ -1019,19 +1190,23 @@ You can defer any number of **slave** models against a **master** model using a The session key is required for deferred bindings. You can think of a session key as of a transaction identifier. The same session key should be used for binding/unbinding relationships and saving the master model. You can generate the session key with PHP `uniqid()` function. Note that both [backend forms](../backend/forms) and the [frontend `form()` function](../markup/function-form) generates a hidden field containing the session key automatically. - $sessionKey = uniqid('session_key', true); +```php +$sessionKey = uniqid('session_key', true); +``` ### Defer a relation binding The comment in the next example will not be added to the post unless the post is saved. - $comment = new Comment; - $comment->content = "Hello world!"; - $comment->save(); +```php +$comment = new Comment; +$comment->content = "Hello world!"; +$comment->save(); - $post = new Post; - $post->comments()->add($comment, $sessionKey); +$post = new Post; +$post->comments()->add($comment, $sessionKey); +``` > **NOTE**: the `$post` object has not been saved but the relationship will be created if the saving happens. @@ -1040,50 +1215,64 @@ The comment in the next example will not be added to the post unless the post is The comment in the next example will not be deleted unless the post is saved. - $comment = Comment::find(1); - $post = Post::find(1); - $post->comments()->remove($comment, $sessionKey); +```php +$comment = Comment::find(1); +$post = Post::find(1); +$post->comments()->remove($comment, $sessionKey); +``` ### List all bindings Use the `withDeferred` method of a relation to load all records, including deferred. The results will include existing relations as well. - $post->comments()->withDeferred($sessionKey)->get(); +```php +$post->comments()->withDeferred($sessionKey)->get(); +``` ### Cancel all bindings It's a good idea to cancel deferred binding and delete the slave objects rather than leaving them as orphans. - $post->cancelDeferred($sessionKey); +```php +$post->cancelDeferred($sessionKey); +``` ### Commit all bindings You can commit (bind or unbind) all deferred bindings when you save the master model by providing the session key with the second argument of the `save` method. - $post = new Post; - $post->title = "First blog post"; - $post->save(null, $sessionKey); +```php +$post = new Post; +$post->title = "First blog post"; +$post->save(null, $sessionKey); +``` The same approach works with the model's `create` method: - $post = Post::create(['title' => 'First blog post'], $sessionKey); +```php +$post = Post::create(['title' => 'First blog post'], $sessionKey); +``` ### Lazily commit bindings If you are unable to supply the `$sessionKey` when saving, you can commit the bindings at any time using the the next code: - $post->commitDeferred($sessionKey); +```php +$post->commitDeferred($sessionKey); +``` ### Clean up orphaned bindings Destroys all bindings that have not been committed and are older than 1 day: - Winter\Storm\Database\Models\DeferredBinding::cleanUp(1); +```php +Winter\Storm\Database\Models\DeferredBinding::cleanUp(1); +``` > **NOTE:** Winter automatically destroys deferred bindings that are older than 5 days. It happens when a backend user logs into the system. @@ -1092,13 +1281,15 @@ Destroys all bindings that have not been committed and are older than 1 day: Sometimes you might need to disable deferred binding entirely for a given model, for instance if you are loading it from a separate database connection. In order to do that, you need to make sure that the model's `sessionKey` property is `null` before the pre and post deferred binding hooks in the internal save method are run. To do that, you can bind to the model's `model.saveInternal` event: - public function __construct() - { - $result = parent::__construct(...func_get_args()); - $this->bindEvent('model.saveInternal', function () { - $this->sessionKey = null; - }); - return $result; - } +```php +public function __construct() +{ + $result = parent::__construct(...func_get_args()); + $this->bindEvent('model.saveInternal', function () { + $this->sessionKey = null; + }); + return $result; +} +``` > **NOTE:** This will disable deferred binding entirely for any model's you apply this override to. diff --git a/database-serialization.md b/database-serialization.md index afaeaa34..b20e87aa 100644 --- a/database-serialization.md +++ b/database-serialization.md @@ -17,95 +17,113 @@ When building JSON APIs, you will often need to convert your models and relation To convert a model and its loaded [relationships](relations) to an array, you may use the `toArray` method. This method is recursive, so all attributes and all relations (including the relations of relations) will be converted to arrays: - $user = User::with('roles')->first(); +```php +$user = User::with('roles')->first(); - return $user->toArray(); +return $user->toArray(); +``` You may also convert [collections](collection) to arrays: - $users = User::all(); +```php +$users = User::all(); - return $users->toArray(); +return $users->toArray(); +``` #### Converting a model to JSON To convert a model to JSON, you may use the `toJson` method. Like `toArray`, the `toJson` method is recursive, so all attributes and relations will be converted to JSON: - $user = User::find(1); +```php +$user = User::find(1); - return $user->toJson(); +return $user->toJson(); +``` Alternatively, you may cast a model or collection to a string, which will automatically call the `toJson` method: - $user = User::find(1); +```php +$user = User::find(1); - return (string) $user; +return (string) $user; +``` Since models and collections are converted to JSON when cast to a string, you can return Model objects directly from your application's routes, AJAX handlers or controllers: - Route::get('users', function () { - return User::all(); - }); +```php +Route::get('users', function () { + return User::all(); +}); +``` ## Hiding attributes from JSON Sometimes you may wish to limit the attributes, such as passwords, that are included in your model's array or JSON representation. To do so, add a `$hidden` property definition to your model: - ## Appending values to JSON Occasionally, you may need to add array attributes that do not have a corresponding column in your database. To do so, first define an [accessor](../database/mutators) for the value: - class User extends Model +```php +class User extends Model +{ + /** + * Get the administrator flag for the user. + * + * @return bool + */ + public function getIsAdminAttribute() { - /** - * Get the administrator flag for the user. - * - * @return bool - */ - public function getIsAdminAttribute() - { - return $this->attributes['admin'] == 'yes'; - } + return $this->attributes['admin'] == 'yes'; } +} +``` Once you have created the accessor, add the attribute name to the `appends` property on the model: - class User extends Model - { - /** - * The accessors to append to the model's array form. - * - * @var array - */ - protected $appends = ['is_admin']; - } +```php +class User extends Model +{ + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = ['is_admin']; +} +``` Once the attribute has been added to the `appends` list, it will be included in both the model's array and JSON forms. Attributes in the `appends` array will also respect the `visible` and `hidden` settings configured on the model. diff --git a/database-structure.md b/database-structure.md index 9bb2f31d..41cd7b5b 100644 --- a/database-structure.md +++ b/database-structure.md @@ -23,43 +23,47 @@ Migrations and seed files allow you to build, modify and populate database table A migration file should define a class that extends the `Winter\Storm\Database\Updates\Migration` class and contains two methods: `up` and `down`. The `up` method is used to add new tables, columns, or indexes to your database, while the `down` method should simply reverse the operations performed by the `up` method. Within both of these methods you may use the [schema builder](#creating-tables) to expressively create and modify tables. For example, let's look at a sample migration that creates a `winter_blog_posts` table: - engine = 'InnoDB'; - $table->increments('id'); - $table->string('title'); - $table->string('slug')->index(); - $table->text('excerpt')->nullable(); - $table->text('content'); - $table->timestamp('published_at')->nullable(); - $table->boolean('is_published')->default(false); - $table->timestamps(); - }); - } - - public function down() - { - Schema::drop('winter_blog_posts'); - } + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->string('title'); + $table->string('slug')->index(); + $table->text('excerpt')->nullable(); + $table->text('content'); + $table->timestamp('published_at')->nullable(); + $table->boolean('is_published')->default(false); + $table->timestamps(); + }); + } + + public function down() + { + Schema::drop('winter_blog_posts'); } +} +``` ### Creating tables To create a new database table, use the `create` method on the `Schema` facade. The `create` method accepts two arguments. The first is the name of the table, while the second is a `Closure` which receives an object used to define the new table: - Schema::create('users', function ($table) { - $table->increments('id'); - }); +```php +Schema::create('users', function ($table) { + $table->increments('id'); +}); +``` Of course, when creating the table, you may use any of the schema builder's [column methods](#creating-columns) to define the table's columns. @@ -67,51 +71,63 @@ Of course, when creating the table, you may use any of the schema builder's [col You may easily check for the existence of a table or column using the `hasTable` and `hasColumn` methods: - if (Schema::hasTable('users')) { - // - } +```php +if (Schema::hasTable('users')) { + // +} - if (Schema::hasColumn('users', 'email')) { - // - } +if (Schema::hasColumn('users', 'email')) { + // +} +``` #### Connection & storage engine If you want to perform a schema operation on a database connection that is not your default connection, use the `connection` method: - Schema::connection('foo')->create('users', function ($table) { - $table->increments('id'); - }); +```php +Schema::connection('foo')->create('users', function ($table) { + $table->increments('id'); +}); +``` To set the storage engine for a table, set the `engine` property on the schema builder: - Schema::create('users', function ($table) { - $table->engine = 'InnoDB'; +```php +Schema::create('users', function ($table) { + $table->engine = 'InnoDB'; - $table->increments('id'); - }); + $table->increments('id'); +}); +``` ### Renaming / dropping tables To rename an existing database table, use the `rename` method: - Schema::rename($from, $to); +```php +Schema::rename($from, $to); +``` To drop an existing table, you may use the `drop` or `dropIfExists` methods: - Schema::drop('users'); +```php +Schema::drop('users'); - Schema::dropIfExists('users'); +Schema::dropIfExists('users'); +``` ### Creating columns To update an existing table, we will use the `table` method on the `Schema` facade. Like the `create` method, the `table` method accepts two arguments, the name of the table and a `Closure` that receives an object we can use to add columns to the table: - Schema::table('users', function ($table) { - $table->string('email'); - }); +```php +Schema::table('users', function ($table) { + $table->string('email'); +}); +``` #### Available Column Types @@ -154,9 +170,11 @@ Command | Description In addition to the column types listed above, there are several other column "modifiers" which you may use while adding the column. For example, to make the column "nullable", you may use the `nullable` method: - Schema::table('users', function ($table) { - $table->string('email')->nullable(); - }); +```php +Schema::table('users', function ($table) { + $table->string('email')->nullable(); +}); +``` Below is a list of all the available column modifiers. This list does not include the [index modifiers](#creating-indexes): @@ -174,24 +192,30 @@ Modifier | Description The `change` method allows you to modify an existing column to a new type, or modify the column's attributes. For example, you may wish to increase the size of a string column. To see the `change` method in action, let's increase the size of the `name` column from 25 to 50: - Schema::table('users', function ($table) { - $table->string('name', 50)->change(); - }); +```php +Schema::table('users', function ($table) { + $table->string('name', 50)->change(); +}); +``` We could also modify a column to be nullable: - Schema::table('users', function ($table) { - $table->string('name', 50)->nullable()->change(); - }); +```php +Schema::table('users', function ($table) { + $table->string('name', 50)->nullable()->change(); +}); +``` #### Renaming columns To rename a column, you may use the `renameColumn` method on the Schema builder: - Schema::table('users', function ($table) { - $table->renameColumn('from', 'to'); - }); +```php +Schema::table('users', function ($table) { + $table->renameColumn('from', 'to'); +}); +``` > **NOTE:** Renaming columns in a table with a `enum` column is not currently supported. @@ -200,34 +224,45 @@ To rename a column, you may use the `renameColumn` method on the Schema builder: To drop a column, use the `dropColumn` method on the Schema builder: - Schema::table('users', function ($table) { - $table->dropColumn('votes'); - }); +```php +Schema::table('users', function ($table) { + $table->dropColumn('votes'); +}); +``` You may drop multiple columns from a table by passing an array of column names to the `dropColumn` method: - Schema::table('users', function ($table) { - $table->dropColumn(['votes', 'avatar', 'location']); - }); +```php +Schema::table('users', function ($table) { + $table->dropColumn(['votes', 'avatar', 'location']); +}); +``` ### Creating indexes The schema builder supports several types of indexes. First, let's look at an example that specifies a column's values should be unique. To create the index, we can simply chain the `unique` method onto the column definition: - $table->string('email')->unique(); +```php +$table->string('email')->unique(); +``` Alternatively, you may create the index after defining the column. For example: - $table->unique('email'); +```php +$table->unique('email'); +``` You may even pass an array of columns to an index method to create a compound index: - $table->index(['account_id', 'created_at']); - +```php +$table->index(['account_id', 'created_at']); +``` In most cases you should specify a name for the index manually as the second argument, to avoid the system automatically generating one that is too long: - $table->index(['account_id', 'created_at'], 'account_created'); +```php +$table->index(['account_id', 'created_at'], 'account_created'); +``` #### Available index types @@ -254,81 +289,95 @@ Command | Description There is also support for creating foreign key constraints, which are used to force referential integrity at the database level. For example, let's define a `user_id` column on the `posts` table that references the `id` column on a `users` table: - Schema::table('posts', function ($table) { - $table->integer('user_id')->unsigned(); +```php +Schema::table('posts', function ($table) { + $table->integer('user_id')->unsigned(); - $table->foreign('user_id')->references('id')->on('users'); - }); + $table->foreign('user_id')->references('id')->on('users'); +}); +``` As before, you may specify a name for the constraint manually by passing a second argument to the `foreign` method: - $table->foreign('user_id', 'user_foreign') - ->references('id') - ->on('users'); +```php +$table->foreign('user_id', 'user_foreign') + ->references('id') + ->on('users'); +``` You may also specify the desired action for the "on delete" and "on update" properties of the constraint: - $table->foreign('user_id') - ->references('id') - ->on('users') - ->onDelete('cascade'); +```php +$table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); +``` To drop a foreign key, you may use the `dropForeign` method. Foreign key constraints use the same naming convention as indexes. So, if one is not specified manually, we will concatenate the table name and the columns in the constraint then suffix the name with "_foreign": - $table->dropForeign('posts_user_id_foreign'); +```php +$table->dropForeign('posts_user_id_foreign'); +``` ## Seeder structure Like migration files, a seeder class only contains one method by default: `run`and should extend the `Seeder` class. The `run` method is called when the update process is executed. Within this method, you may insert data into your database however you wish. You may use the [query builder](../database/query) to manually insert data or you may use your [model classes](../database/model). In the example below, we'll create a new user using the `User` model inside the `run` method: - 'user@example.com', - 'login' => 'user', - 'password' => 'password123', - 'password_confirmation' => 'password123', - 'first_name' => 'Actual', - 'last_name' => 'Person', - 'is_activated' => true - ]); - } - } +```php +insert([ + $user = User::create([ 'email' => 'user@example.com', 'login' => 'user', - [...] + 'password' => 'password123', + 'password_confirmation' => 'password123', + 'first_name' => 'Actual', + 'last_name' => 'Person', + 'is_activated' => true ]); } +} +``` + +Alternatively, the same can be achieved using the `Db::table` [query builder](../database/query) method: + +```php +public function run() +{ + $user = Db::table('users')->insert([ + 'email' => 'user@example.com', + 'login' => 'user', + [...] + ]); +} +``` ### Calling additional seeders Within the `DatabaseSeeder` class, you may use the `call` method to execute additional seed classes. Using the `call` method allows you to break up your database seeding into multiple files so that no single seeder class becomes overwhelmingly large. Simply pass the name of the seeder class you wish to run: - /** - * Run the database seeds. - * - * @return void - */ - public function run() - { - Model::unguard(); - - $this->call('Acme\Users\Updates\UserTableSeeder'); - $this->call('Acme\Users\Updates\PostsTableSeeder'); - $this->call('Acme\Users\Updates\CommentsTableSeeder'); - } +```php +/** + * Run the database seeds. + * + * @return void + */ +public function run() +{ + Model::unguard(); + + $this->call('Acme\Users\Updates\UserTableSeeder'); + $this->call('Acme\Users\Updates\PostsTableSeeder'); + $this->call('Acme\Users\Updates\CommentsTableSeeder'); +} +``` diff --git a/database-traits.md b/database-traits.md index b99e33fd..d5ae084d 100644 --- a/database-traits.md +++ b/database-traits.md @@ -19,49 +19,57 @@ Model traits are used to implement common functionality. Hashed attributes are hashed immediately when the attribute is first set on the model. To hash attributes in your model, apply the `Winter\Storm\Database\Traits\Hashable` trait and declare a `$hashable` property with an array containing the attributes to hash. - class User extends Model - { - use \Winter\Storm\Database\Traits\Hashable; - - /** - * @var array List of attributes to hash. - */ - protected $hashable = ['password']; - } +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Hashable; + + /** + * @var array List of attributes to hash. + */ + protected $hashable = ['password']; +} +``` ## Purgeable Purged attributes will not be saved to the database when a model is created or updated. To purge attributes in your model, apply the `Winter\Storm\Database\Traits\Purgeable` trait and declare a `$purgeable` property with an array containing the attributes to purge. - class User extends Model - { - use \Winter\Storm\Database\Traits\Purgeable; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Purgeable; - /** - * @var array List of attributes to purge. - */ - protected $purgeable = ['password_confirmation']; - } + /** + * @var array List of attributes to purge. + */ + protected $purgeable = ['password_confirmation']; +} +``` The defined attributes will be purged when the model is saved, before the [model events](#model-events) are triggered, including validation. Use the `getOriginalPurgeValue` to find a value that was purged. - return $user->getOriginalPurgeValue('password_confirmation'); +```php +return $user->getOriginalPurgeValue('password_confirmation'); +``` ## Encryptable Similar to the [hashable trait](#hashable), encrypted attributes are encrypted when set but also decrypted when an attribute is retrieved. To encrypt attributes in your model, apply the `Winter\Storm\Database\Traits\Encryptable` trait and declare a `$encryptable` property with an array containing the attributes to encrypt. - class User extends Model - { - use \Winter\Storm\Database\Traits\Encryptable; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Encryptable; - /** - * @var array List of attributes to encrypt. - */ - protected $encryptable = ['api_key', 'api_secret']; - } + /** + * @var array List of attributes to encrypt. + */ + protected $encryptable = ['api_key', 'api_secret']; +} +``` > **NOTE:** Encrypted attributes will be serialized and unserialized as a part of the encryption / decryption process. Do not make an attribute that is `encryptable` also [`jsonable`](model#standard-properties) at the same time as the `jsonable` process will attempt to decode a value that has already been unserialized by the encryptor. @@ -70,37 +78,45 @@ Similar to the [hashable trait](#hashable), encrypted attributes are encrypted w Slugs are meaningful codes that are commonly used in page URLs. To automatically generate a unique slug for your model, apply the `Winter\Storm\Database\Traits\Sluggable` trait and declare a `$slugs` property. - class User extends Model - { - use \Winter\Storm\Database\Traits\Sluggable; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Sluggable; - /** - * @var array Generate slugs for these attributes. - */ - protected $slugs = ['slug' => 'name']; - } + /** + * @var array Generate slugs for these attributes. + */ + protected $slugs = ['slug' => 'name']; +} +``` The `$slugs` property should be an array where the key is the destination column for the slug and the value is the source string used to generate the slug. In the above example, if the `name` column was set to **Cheyenne**, as a result the `slug` column would be set to **cheyenne**, **cheyenne-2**, or **cheyenne-3**, etc before the model is created. To generate a slug from multiple sources, pass another array as the source value: - protected $slugs = [ - 'slug' => ['first_name', 'last_name'] - ]; +```php +protected $slugs = [ + 'slug' => ['first_name', 'last_name'] +]; +``` Slugs are only generated when a model first created. To override or disable this functionality, simply set the slug attribute manually: - $user = new User; - $user->name = 'Remy'; - $user->slug = 'custom-slug'; - $user->save(); // Slug will not be generated +```php +$user = new User; +$user->name = 'Remy'; +$user->slug = 'custom-slug'; +$user->save(); // Slug will not be generated +``` Use the `slugAttributes` method to regenerate slugs when updating a model: - $user = User::find(1); - $user->slug = null; - $user->slugAttributes(); - $user->save(); +```php +$user = User::find(1); +$user->slug = null; +$user->slugAttributes(); +$user->save(); +``` ### Sluggable with SoftDelete trait @@ -110,76 +126,91 @@ You might want to prevent slug duplication when recovering soft deleted models. Set the `$allowTrashedSlugs` attribute to `true` in order to take into account soft deleted records when generating new slugs. - protected $allowTrashedSlugs = true; +```php +protected $allowTrashedSlugs = true; +``` ## Revisionable Winter models can record the history of changes in values by storing revisions. To store revisions for your model, apply the `Winter\Storm\Database\Traits\Revisionable` trait and declare a `$revisionable` property with an array containing the attributes to monitor for changes. You also need to define a `$morphMany` [model relation](relations) called `revision_history` that refers to the `System\Models\Revision` class with the name `revisionable`, this is where the revision history data is stored. - class User extends Model - { - use \Winter\Storm\Database\Traits\Revisionable; - - /** - * @var array Monitor these attributes for changes. - */ - protected $revisionable = ['name', 'email']; - - /** - * @var array Relations - */ - public $morphMany = [ - 'revision_history' => ['System\Models\Revision', 'name' => 'revisionable'] - ]; - } +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Revisionable; -By default 500 records will be kept, however this can be modified by declaring a `$revisionableLimit` property on the model with a new limit value. + /** + * @var array Monitor these attributes for changes. + */ + protected $revisionable = ['name', 'email']; /** - * @var int Maximum number of revision records to keep. + * @var array Relations */ - public $revisionableLimit = 8; + public $morphMany = [ + 'revision_history' => ['System\Models\Revision', 'name' => 'revisionable'] + ]; +} +``` + +By default 500 records will be kept, however this can be modified by declaring a `$revisionableLimit` property on the model with a new limit value. + +```php +/** + * @var int Maximum number of revision records to keep. + */ +public $revisionableLimit = 8; +``` The revision history can be accessed like any other relation: - $history = User::find(1)->revision_history; +```php +$history = User::find(1)->revision_history; - foreach ($history as $record) { - echo $record->field . ' updated '; - echo 'from ' . $record->old_value; - echo 'to ' . $record->new_value; - } +foreach ($history as $record) { + echo $record->field . ' updated '; + echo 'from ' . $record->old_value; + echo 'to ' . $record->new_value; +} +``` The revision record optionally supports a user relationship using the `user_id` attribute. You may include a `getRevisionableUser` method in your model to keep track of the user that made the modification. - public function getRevisionableUser() - { - return BackendAuth::getUser()->id; - } +```php +public function getRevisionableUser() +{ + return BackendAuth::getUser()->id; +} +``` ## Sortable Sorted models will store a number value in `sort_order` which maintains the sort order of each individual model in a collection. To store a sort order for your models, apply the `Winter\Storm\Database\Traits\Sortable` trait and ensure that your schema has a column defined for it to use (example: `$table->integer('sort_order')->default(0);`). - class User extends Model - { - use \Winter\Storm\Database\Traits\Sortable; - } - +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Sortable; +} +``` You may modify the key name used to identify the sort order by defining the `SORT_ORDER` constant: - const SORT_ORDER = 'my_sort_order_column'; +```php +const SORT_ORDER = 'my_sort_order_column'; +``` Use the `setSortableOrder` method to set the orders on a single record or multiple records. - // Sets the order of the user to 1... - $user->setSortableOrder($user->id, 1); +```php +// Sets the order of the user to 1... +$user->setSortableOrder($user->id, 1); - // Sets the order of records 1, 2, 3 to 3, 2, 1 respectively... - $user->setSortableOrder([1, 2, 3], [3, 2, 1]); +// Sets the order of records 1, 2, 3 to 3, 2, 1 respectively... +$user->setSortableOrder([1, 2, 3], [3, 2, 1]); +``` > **NOTE:** If adding this trait to a model where data (rows) already existed previously, the data set may need to be initialized before this trait will work correctly. To do so, either manually update each row's `sort_order` column or run a query against the data to copy the record's `id` column to the `sort_order` column (ex. `UPDATE myvendor_myplugin_mymodelrecords SET sort_order = id`). @@ -188,95 +219,121 @@ Use the `setSortableOrder` method to set the orders on a single record or multip A simple tree model will use the `parent_id` column maintain a parent and child relationship between models. To use the simple tree, apply the `Winter\Storm\Database\Traits\SimpleTree` trait. - class Category extends Model - { - use \Winter\Storm\Database\Traits\SimpleTree; - } +```php +class Category extends Model +{ + use \Winter\Storm\Database\Traits\SimpleTree; +} +``` This trait will automatically inject two [model relations](../database/relations) called `parent` and `children`, it is the equivalent of the following definitions: - public $belongsTo = [ - 'parent' => ['User', 'key' => 'parent_id'], - ]; +```php +public $belongsTo = [ + 'parent' => ['User', 'key' => 'parent_id'], +]; - public $hasMany = [ - 'children' => ['User', 'key' => 'parent_id'], - ]; +public $hasMany = [ + 'children' => ['User', 'key' => 'parent_id'], +]; +``` You may modify the key name used to identify the parent by defining the `PARENT_ID` constant: - const PARENT_ID = 'my_parent_column'; +```php +const PARENT_ID = 'my_parent_column'; +``` Collections of models that use this trait will return the type of `Winter\Storm\Database\TreeCollection` which adds the `toNested` method. To build an eager loaded tree structure, return the records with the relations eager loaded. - Category::all()->toNested(); +```php +Category::all()->toNested(); +``` ### Rendering In order to render all levels of items and their children, you can use recursive processing - {% macro renderChildren(item) %} - {% import _self as SELF %} - {% if item.children is not empty %} -
    - {% for child in item.children %} -
  • {{ child.name }}{{ SELF.renderChildren(child) | raw }}
  • - {% endfor %} -
- {% endif %} - {% endmacro %} - +```twig +{% macro renderChildren(item) %} {% import _self as SELF %} - {{ SELF.renderChildren(category) | raw }} + {% if item.children is not empty %} +
    + {% for child in item.children %} +
  • {{ child.name }}{{ SELF.renderChildren(child) | raw }}
  • + {% endfor %} +
+ {% endif %} +{% endmacro %} + +{% import _self as SELF %} +{{ SELF.renderChildren(category) | raw }} +``` ## Nested Tree The [nested set model](https://en.wikipedia.org/wiki/Nested_set_model) is an advanced technique for maintaining hierachies among models using `parent_id`, `nest_left`, `nest_right`, and `nest_depth` columns. To use a nested set model, apply the `Winter\Storm\Database\Traits\NestedTree` trait. All of the features of the `SimpleTree` trait are inherently available in this model. - class Category extends Model - { - use \Winter\Storm\Database\Traits\NestedTree; - } +```php +class Category extends Model +{ + use \Winter\Storm\Database\Traits\NestedTree; +} +``` ### Creating a root node By default, all nodes are created as roots: - $root = Category::create(['name' => 'Root category']); +```php +$root = Category::create(['name' => 'Root category']); +``` Alternatively, you may find yourself in the need of converting an existing node into a root node: - $node->makeRoot(); +```php +$node->makeRoot(); +``` You may also nullify it's `parent_id` column which works the same as `makeRoot'. - $node->parent_id = null; - $node->save(); +```php +$node->parent_id = null; +$node->save(); +``` ### Inserting nodes You can insert new nodes directly by the relation: - $child1 = $root->children()->create(['name' => 'Child 1']); +```php +$child1 = $root->children()->create(['name' => 'Child 1']); +``` Or use the `makeChildOf` method for existing nodes: - $child2 = Category::create(['name' => 'Child 2']); - $child2->makeChildOf($root); +```php +$child2 = Category::create(['name' => 'Child 2']); +$child2->makeChildOf($root); +``` ### Deleting nodes When a node is deleted with the `delete` method, all descendants of the node will also be deleted. Note that the delete [model events](../database/model#model-events) will not be fired for the child models. - $child1->delete(); +```php +$child1->delete(); +``` ### Getting the nesting level of a node The `getLevel` method will return current nesting level, or depth, of a node. - // 0 when root - $node->getLevel() +```php +// 0 when root +$node->getLevel() +``` ### Moving nodes around @@ -294,39 +351,45 @@ There are several methods for moving nodes around: Winter models uses the built-in [Validator class](../services/validation). The validation rules are defined in the model class as a property named `$rules` and the class must use the trait `Winter\Storm\Database\Traits\Validation`: - class User extends Model - { - use \Winter\Storm\Database\Traits\Validation; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Validation; - public $rules = [ - 'name' => 'required|between:4,16', - 'email' => 'required|email', - 'password' => 'required|alpha_num|between:4,8|confirmed', - 'password_confirmation' => 'required|alpha_num|between:4,8' - ]; - } + public $rules = [ + 'name' => 'required|between:4,16', + 'email' => 'required|email', + 'password' => 'required|alpha_num|between:4,8|confirmed', + 'password_confirmation' => 'required|alpha_num|between:4,8' + ]; +} +``` > **NOTE**: You're free to use the [array syntax](../services/validation#validating-arrays) for validation rules as well. - class User extends Model - { - use \Winter\Storm\Database\Traits\Validation; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Validation; - public $rules = [ - 'links.*.url' => 'required|url', - 'links.*.anchor' => 'required' - ]; - } + public $rules = [ + 'links.*.url' => 'required|url', + 'links.*.anchor' => 'required' + ]; +} +``` Models validate themselves automatically when the `save` method is called. - $user = new User; - $user->name = 'Actual Person'; - $user->email = 'a.person@example.com'; - $user->password = 'passw0rd'; +```php +$user = new User; +$user->name = 'Actual Person'; +$user->email = 'a.person@example.com'; +$user->password = 'passw0rd'; - // Returns false if model is invalid - $success = $user->save(); +// Returns false if model is invalid +$success = $user->save(); +``` > **NOTE:** You can also validate a model at any time using the `validate` method. @@ -342,41 +405,47 @@ When a model fails to validate, a `Illuminate\Support\MessageBag` object is atta The `forceSave` method validates the model and saves regardless of whether or not there are validation errors. - $user = new User; +```php +$user = new User; - // Creates a user without validation - $user->forceSave(); +// Creates a user without validation +$user->forceSave(); +``` ### Custom error messages Just like the Validator class, you can set custom error messages using the [same syntax](../services/validation#custom-error-messages). - class User extends Model - { - public $customMessages = [ - 'required' => 'The :attribute field is required.', - ... - ]; - } +```php +class User extends Model +{ + public $customMessages = [ + 'required' => 'The :attribute field is required.', + ... + ]; +} +``` You can also add custom error messages to the array syntax for validation rules as well. - class User extends Model - { - use \Winter\Storm\Database\Traits\Validation; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\Validation; - public $rules = [ - 'links.*.url' => 'required|url', - 'links.*.anchor' => 'required' - ]; + public $rules = [ + 'links.*.url' => 'required|url', + 'links.*.anchor' => 'required' + ]; - public $customMessages = [ - 'links.*.url.required' => 'The url is required', - 'links.*.url.*' => 'The url needs to be a valid url' - 'links.*.anchor.required' => 'The anchor text is required', - ]; - } + public $customMessages = [ + 'links.*.url.required' => 'The url is required', + 'links.*.url.*' => 'The url needs to be a valid url' + 'links.*.anchor.required' => 'The anchor text is required', + ]; +} +``` In the above example you can write custom error messages to specific validation rules (here we used: `required`). Or you can use a `*` to select everything else (here we added a custom message to the `url` validation rule using the `*`). @@ -385,26 +454,30 @@ In the above example you can write custom error messages to specific validation You may also set custom attribute names with the `$attributeNames` array. - class User extends Model - { - public $attributeNames = [ - 'email' => 'Email Address', - ... - ]; - } +```php +class User extends Model +{ + public $attributeNames = [ + 'email' => 'Email Address', + ... + ]; +} +``` ### Dynamic validation rules You can apply rules dynamically by overriding the `beforeValidate` [model event](../database/model#events) method. Here we check if the `is_remote` attribute is `false` and then dynamically set the `latitude` and `longitude` attributes to be required fields. - public function beforeValidate() - { - if (!$this->is_remote) { - $this->rules['latitude'] = 'required'; - $this->rules['longitude'] = 'required'; - } +```php +public function beforeValidate() +{ + if (!$this->is_remote) { + $this->rules['latitude'] = 'required'; + $this->rules['longitude'] = 'required'; } +} +``` ### Custom validation rules @@ -416,26 +489,32 @@ You can also create custom validation rules the [same way](../services/validatio When soft deleting a model, it is not actually removed from your database. Instead, a `deleted_at` timestamp is set on the record. To enable soft deletes for a model, apply the `Winter\Storm\Database\Traits\SoftDelete` trait to the model and add the deleted_at column to your `$dates` property: - class User extends Model - { - use \Winter\Storm\Database\Traits\SoftDelete; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\SoftDelete; - protected $dates = ['deleted_at']; - } + protected $dates = ['deleted_at']; +} +``` To add a `deleted_at` column to your table, you may use the `softDeletes` method from a migration: - Schema::table('posts', function ($table) { - $table->softDeletes(); - }); +```php +Schema::table('posts', function ($table) { + $table->softDeletes(); +}); +``` Now, when you call the `delete` method on the model, the `deleted_at` column will be set to the current timestamp. When querying a model that uses soft deletes, the "deleted" models will not be included in query results. To determine if a given model instance has been soft deleted, use the `trashed` method: - if ($user->trashed()) { - // - } +```php +if ($user->trashed()) { + // +} +``` ### Querying soft deleted models @@ -444,62 +523,78 @@ To determine if a given model instance has been soft deleted, use the `trashed` As noted above, soft deleted models will automatically be excluded from query results. However, you may force soft deleted models to appear in a result set using the `withTrashed` method on the query: - $users = User::withTrashed()->where('account_id', 1)->get(); +```php +$users = User::withTrashed()->where('account_id', 1)->get(); +``` The `withTrashed` method may also be used on a [relationship](relations) query: - $flight->history()->withTrashed()->get(); +```php +$flight->history()->withTrashed()->get(); +``` #### Retrieving only soft deleted models The `onlyTrashed` method will retrieve **only** soft deleted models: - $users = User::onlyTrashed()->where('account_id', 1)->get(); +```php +$users = User::onlyTrashed()->where('account_id', 1)->get(); +``` #### Restoring soft deleted models Sometimes you may wish to "un-delete" a soft deleted model. To restore a soft deleted model into an active state, use the `restore` method on a model instance: - $user->restore(); +```php +$user->restore(); +``` You may also use the `restore` method in a query to quickly restore multiple models: - // Restore a single model instance... - User::withTrashed()->where('account_id', 1)->restore(); +```php +// Restore a single model instance... +User::withTrashed()->where('account_id', 1)->restore(); - // Restore all related models... - $user->posts()->restore(); +// Restore all related models... +$user->posts()->restore(); +``` #### Permanently deleting models Sometimes you may need to truly remove a model from your database. To permanently remove a soft deleted model from the database, use the `forceDelete` method: - // Force deleting a single model instance... - $user->forceDelete(); +```php +// Force deleting a single model instance... +$user->forceDelete(); - // Force deleting all related models... - $user->posts()->forceDelete(); +// Force deleting all related models... +$user->posts()->forceDelete(); +``` ### Soft deleting relations When two related models have soft deletes enabled, you can cascade the delete event by defining the `softDelete` option in the [relation definition](../database/relations#detailed-relationships). In this example, if the user model is soft deleted, the comments belonging to that user will also be soft deleted. - class User extends Model - { - use \Winter\Storm\Database\Traits\SoftDelete; +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\SoftDelete; - public $hasMany = [ - 'comments' => ['Acme\Blog\Models\Comment', 'softDelete' => true] - ]; - } + public $hasMany = [ + 'comments' => ['Acme\Blog\Models\Comment', 'softDelete' => true] + ]; +} +``` > **NOTE:** If the related model does not use the soft delete trait, it will be treated the same as the `delete` option and deleted permanently. Under these same conditions, when the primary model is restored, all the related models that use the `softDelete` option will also be restored. - // Restore the user and comments - $user->restore(); +```php +// Restore the user and comments +$user->restore(); +``` ### Soft Delete with Sluggable trait @@ -512,12 +607,14 @@ In order to make the model restoration less painful [checkout the Sluggable sect Nullable attributes are set to `NULL` when left empty. To nullify attributes in your model, apply the `Winter\Storm\Database\Traits\Nullable` trait and declare a `$nullable` property with an array containing the attributes to nullify. - class Product extends Model - { - use \Winter\Storm\Database\Traits\Nullable; +```php +class Product extends Model +{ + use \Winter\Storm\Database\Traits\Nullable; - /** - * @var array Nullable attributes. - */ - protected $nullable = ['sku']; - } + /** + * @var array Nullable attributes. + */ + protected $nullable = ['sku']; +} +``` From fed8f4b09ab29eee5135235157beb39e41d1f11d Mon Sep 17 00:00:00 2001 From: Web-VPF Date: Tue, 28 Dec 2021 08:52:16 +0200 Subject: [PATCH 06/26] Improving usability - events section --- events-introduction.md | 260 +++++++++++++++++++++++++---------------- 1 file changed, 160 insertions(+), 100 deletions(-) diff --git a/events-introduction.md b/events-introduction.md index df369c5f..5f6b8dc9 100644 --- a/events-introduction.md +++ b/events-introduction.md @@ -19,31 +19,41 @@ The `Event` class provides a simple observer implementation, allowing you to subscribe and listen for events in your application. For example, you may listen for when a user signs in and update their last login date. - Event::listen('auth.login', function($user) { - $user->last_login = new DateTime; - $user->save(); - }); +```php +Event::listen('auth.login', function($user) { + $user->last_login = new DateTime; + $user->save(); +}); +``` This is event made available with the `Event::fire` method which is called as part of the user sign in logic, thereby making the logic extensible. - Event::fire('auth.login', [$user]); +```php +Event::fire('auth.login', [$user]); +``` ## Subscribing to events The `Event::listen` method is primarily used to subscribe to events and can be done from anywhere within your application code. The first argument is the event name. - Event::listen('acme.blog.myevent', ...); +```php +Event::listen('acme.blog.myevent', ...); +``` The second argument can be a closure that specifies what should happen when the event is fired. The closure can accept optional some arguments, provided by [the firing event](#events-firing). - Event::listen('acme.blog.myevent', function($arg1, $arg2) { - // Do something - }); +```php +Event::listen('acme.blog.myevent', function($arg1, $arg2) { + // Do something +}); +``` You may also pass a reference to any callable object or a [dedicated event class](#using-classes-as-listeners) and this will be used instead. - Event::listen('auth.login', [$this, 'LoginHandler']); +```php +Event::listen('auth.login', [$this, 'LoginHandler']); +``` > **NOTE**: The callable method can choose to specify all, some or none of the arguments. Either way the event will not throw any errors unless it specifies too many. @@ -52,21 +62,25 @@ You may also pass a reference to any callable object or a [dedicated event class The most common place is the `boot` method of a [Plugin registration file](../plugin/registration#registration-methods). - class Plugin extends PluginBase - { - [...] +```php +class Plugin extends PluginBase +{ + [...] - public function boot() - { - Event::listen(...); - } + public function boot() + { + Event::listen(...); } +} +``` Alternatively, plugins can supply a file named **init.php** in the plugin directory that you can use to place event registration logic. For example: - ### Halting events Sometimes you may wish to stop the propagation of an event to other listeners. You may do so using by returning `false` from your listener: - Event::listen('auth.login', function($event) { - // Handle the event +```php +Event::listen('auth.login', function($event) { + // Handle the event - return false; - }); + return false; +}); +``` ### Wildcard listeners @@ -99,74 +117,98 @@ When registering an event listener, you may use asterisks to specify wildcard li The following listener will handle all events that begin with `foo.`. - Event::listen('foo.*', function($event, $params) { - // Handle the event... - }); +```php +Event::listen('foo.*', function($event, $params) { + // Handle the event... +}); +``` You may use the `Event::firing` method to determine exactly which event was fired: - Event::listen('foo.*', function($event, $params) { - if (Event::firing() === 'foo.bar') { - // ... - } - }); +```php +Event::listen('foo.*', function($event, $params) { + if (Event::firing() === 'foo.bar') { + // ... + } +}); +``` ## Firing events You may use the `Event::fire` method anywhere in your code to make the logic extensible. This means other developers, or even your own internal code, can "hook" to this point of code and inject specific logic. The first argument of should be the event name. - Event::fire('myevent') +```php +Event::fire('myevent') +``` It is always a good idea to prefix event names with your plugin namespace code, this will prevent collisions with other plugins. - Event::fire('acme.blog.myevent'); +```php +Event::fire('acme.blog.myevent'); +``` The second argument is an array of values that will be passed as arguments to [the event listener](#events-subscribing) subscribing to it. - Event::fire('acme.blog.myevent', [$arg1, $arg2]); +```php +Event::fire('acme.blog.myevent', [$arg1, $arg2]); +``` The third argument specifies whether the event should be a [halting event](#subscribing-halting), meaning it should halt if a "non null" value is returned. This argument is set to false by default. - Event::fire('acme.blog.myevent', [...], true); +```php +Event::fire('acme.blog.myevent', [...], true); +``` If the event is halting, the first value returned with be captured. - // Single result, event halted - $result = Event::fire('acme.blog.myevent', [...], true); +```php +// Single result, event halted +$result = Event::fire('acme.blog.myevent', [...], true); +``` Otherwise it returns a collection of all the responses from all the events in the form of an array. - // Multiple results, all events fired - $results = Event::fire('acme.blog.myevent', [...]); +```php +// Multiple results, all events fired +$results = Event::fire('acme.blog.myevent', [...]); +``` ## Passing arguments by reference When processing or filtering over a value passed to an event, you may prefix the variable with `&` to pass it by reference. This allows multiple listeners to manipulate the result and pass it to the next one. - Event::fire('cms.processContent', [&$content]); +```php +Event::fire('cms.processContent', [&$content]); +``` When listening for the event, the argument also needs to be declared with the `&` symbol in the closure definition. In the example below, the `$content` variable will have "AB" appended to the result. - Event::listen('cms.processContent', function (&$content) { - $content = $content . 'A'; - }); +```php +Event::listen('cms.processContent', function (&$content) { + $content = $content . 'A'; +}); - Event::listen('cms.processContent', function (&$content) { - $content = $content . 'B'; - }); +Event::listen('cms.processContent', function (&$content) { + $content = $content . 'B'; +}); +``` ### Queued events Firing events can be deferred in [conjunction with queues](../services/queues). Use the `Event::queue` method to "queue" the event for firing but not fire it immediately. - Event::queue('foo', [$user]); +```php +Event::queue('foo', [$user]); +``` You may use the `Event::flush` method to flush all queued events. - Event::flush('foo'); +```php +Event::flush('foo'); +``` ## Using classes as listeners @@ -178,87 +220,105 @@ In some cases, you may wish to use a class to handle an event rather than a Clos The event class can be registered with the `Event::listen` method like any other, passing the class name as a string. - Event::listen('auth.login', 'LoginHandler'); +```php +Event::listen('auth.login', 'LoginHandler'); +``` By default, the `handle` method on the `LoginHandler` class will be called: - class LoginHandler +```php +class LoginHandler +{ + public function handle($data) { - public function handle($data) - { - // ... - } + // ... } +} +``` If you do not wish to use the default `handle` method, you may specify the method name that should be subscribed. - Event::listen('auth.login', 'LoginHandler@onLogin'); +```php +Event::listen('auth.login', 'LoginHandler@onLogin'); +``` ### Subscribe to entire class Event subscribers are classes that may subscribe to multiple events from within the class itself. Subscribers should define a `subscribe` method, which will be passed an event dispatcher instance. - class UserEventHandler +```php +class UserEventHandler +{ + /** + * Handle user login events. + */ + public function userLogin($event) + { + // ... + } + + /** + * Handle user logout events. + */ + public function userLogout($event) { - /** - * Handle user login events. - */ - public function userLogin($event) - { - // ... - } - - /** - * Handle user logout events. - */ - public function userLogout($event) - { - // ... - } - - /** - * Register the listeners for the subscriber. - * - * @param Illuminate\Events\Dispatcher $events - * @return array - */ - public function subscribe($events) - { - $events->listen('auth.login', 'UserEventHandler@userLogin'); - - $events->listen('auth.logout', 'UserEventHandler@userLogout'); - } + // ... } + /** + * Register the listeners for the subscriber. + * + * @param Illuminate\Events\Dispatcher $events + * @return array + */ + public function subscribe($events) + { + $events->listen('auth.login', 'UserEventHandler@userLogin'); + + $events->listen('auth.logout', 'UserEventHandler@userLogout'); + } +} +``` + Once the subscriber has been defined, it may be registered with the `Event::subscribe` method. - Event::subscribe(new UserEventHandler); +```php +Event::subscribe(new UserEventHandler); +``` You may also use the [Application IoC container](../services/application) to resolve your subscriber. To do so, simply pass the name of your subscriber to the `subscribe` method. - Event::subscribe('UserEventHandler'); +```php +Event::subscribe('UserEventHandler'); +``` ## Event emitter trait Sometimes you want to bind events to a single instance of an object. You may use an alternative event system by implementing the `Winter\Storm\Support\Traits\Emitter` trait inside your class. - class UserManager - { - use \Winter\Storm\Support\Traits\Emitter; - } +```php +class UserManager +{ + use \Winter\Storm\Support\Traits\Emitter; +} +``` This trait provides a method to listen for events with `bindEvent`. - $manager = new UserManager; - $manager->bindEvent('user.beforeRegister', function($user) { - // Check if the $user is a spammer - }); +```php +$manager = new UserManager; +$manager->bindEvent('user.beforeRegister', function($user) { + // Check if the $user is a spammer +}); +``` The `fireEvent` method is used to fire events. - $manager = new UserManager; - $manager->fireEvent('user.beforeRegister', [$user]); +```php +$manager = new UserManager; +$manager->fireEvent('user.beforeRegister', [$user]); +``` These events will only occur on the local object as opposed to globally. From 67d54d25162a2e0734e0ada6d9d66c133d481b7a Mon Sep 17 00:00:00 2001 From: WebVPF <61043464+WebVPF@users.noreply.github.com> Date: Tue, 28 Dec 2021 09:11:00 +0200 Subject: [PATCH 07/26] add spaces backend-reorder.md Co-authored-by: Ben Thomson --- backend-reorder.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend-reorder.md b/backend-reorder.md index d3c02677..710f8212 100644 --- a/backend-reorder.md +++ b/backend-reorder.md @@ -56,8 +56,8 @@ modelClass: Acme\Shop\Models\Category # Toolbar widget configuration toolbar: - # Partial for toolbar buttons - buttons: reorder_toolbar + # Partial for toolbar buttons + buttons: reorder_toolbar ``` The configuration options listed below can be used. From 8fe3b7eaa1c6394eac0a52f34c6b0a01975cd0cb Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 28 Dec 2021 10:08:57 -0600 Subject: [PATCH 08/26] Update backend-reorder.md --- backend-reorder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-reorder.md b/backend-reorder.md index 710f8212..2fafb90b 100644 --- a/backend-reorder.md +++ b/backend-reorder.md @@ -100,6 +100,6 @@ The lookup query for the list [database model](../database/model) can be extende ```php public function reorderExtendQuery($query) { - $query->withTrashed(); + $query->withTrashed(); } ``` From 03bdca4b13201025247ce063380604b2b18110ca Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 28 Dec 2021 10:10:05 -0600 Subject: [PATCH 09/26] Update backend-users.md --- backend-users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-users.md b/backend-users.md index 10d7fc18..4335e25d 100644 --- a/backend-users.md +++ b/backend-users.md @@ -161,7 +161,7 @@ if ($this->user->hasPermission([ You can also use the methods in the backend views for hiding user interface elements. The next examples demonstrates how you can hide a button on the Edit Category [backend form](forms): -```html +```php user->hasAccess('acme.blog.delete_categories')): ?>