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 3388bd39..4ddb9b4a 100644 --- a/ajax-javascript-api.md +++ b/ajax-javascript-api.md @@ -12,147 +12,187 @@ 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. -**confirm** | the confirmation string. If set, the confirmation is displayed before the request is sent. If the user clicks the Cancel button, the request cancels. -**data** | an optional object specifying data to be sent to the server along with the form data: {var: 'value'}. When `files` is true, you may also include files to be uploaded in this object by using [`Blob` objects](https://developer.mozilla.org/en-US/docs/Web/API/Blob). To specify the filename of any `Blob` objects, simply set the `filename` property on the `Blob` object. (Ex. `var blob = new Blob(variable); blob.filename = 'test.txt'; var data = {'uploaded_file': blob};`) -**redirect** | string specifying an URL to redirect the browser to after the successful request. -**beforeUpdate** | a callback function to execute before page elements are updated. The function gets 3 parameters: the data object received from the server, text status string, and the jqXHR object. The `this` variable inside the function resolves to the request content - an object containing 2 properties: `handler` and `options` representing the original request() parameters. -**success** | a callback function to execute in case of a successful request. If this option is supplied it overrides the default framework's functionality: the elements are not updated, the `beforeUpdate` event is not triggered, the `ajaxUpdate` and `ajaxUpdateComplete` events are not triggered. The event handler gets 3 arguments: the data object received from the server, the text status string and the jqXHR object. However, you can still call the default framework functionality calling `this.success(...)` inside your function. -**error** | a callback function execute in case of an error. By default the alert message is displayed. If this option is overridden the alert message won't be displayed. The handler gets 3 parameters: the jqXHR object, the text status string and the error object - see [jQuery AJAX function](http://api.jquery.com/jQuery.ajax/). -**complete** | a callback function execute in case of a success or an error. -**form** | a form element to use for sourcing the form data sent with the request, either passed as a selector string or a form element. -**flash** | when true, instructs the server to clear and send any flash messages with the response. default: false -**files** | when true, the request will accept file uploads, this requires `FormData` interface support by the browser. default: false -**browserValidate** | when true, browser-based client side validation will be performed on the request before submitting. This only applies to requests triggered in the context of a `` element. **NOTE:** This form of validation does not play nice with complex forms where validated fields might not be visible to the user 100% of the time. Recommend that you avoid using it on anything but the most simple forms. -**loading** | an optional string or object to be displayed when a request runs. The string should be a CSS selector for an element, the object should support the `show()` and `hide()` functions to manage the visibility. You may pass the global object `$.wn.stripeLoadIndicator` when using the [framework extras](../ajax/extras). +`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. +`confirm` | the confirmation string. If set, the confirmation is displayed before the request is sent. If the user clicks the Cancel button, the request cancels. +`data` | an optional object specifying data to be sent to the server along with the form data: {var: 'value'}. When `files` is true, you may also include files to be uploaded in this object by using [`Blob` objects](https://developer.mozilla.org/en-US/docs/Web/API/Blob). To specify the filename of any `Blob` objects, simply set the `filename` property on the `Blob` object. (Ex. `var blob = new Blob(variable); blob.filename = 'test.txt'; var data = {'uploaded_file': blob};`) +`redirect` | string specifying an URL to redirect the browser to after the successful request. +`beforeUpdate` | a callback function to execute before page elements are updated. The function gets 3 parameters: the data object received from the server, text status string, and the jqXHR object. The `this` variable inside the function resolves to the request content - an object containing 2 properties: `handler` and `options` representing the original request() parameters. +`success` | a callback function to execute in case of a successful request. If this option is supplied it overrides the default framework's functionality: the elements are not updated, the `beforeUpdate` event is not triggered, the `ajaxUpdate` and `ajaxUpdateComplete` events are not triggered. The event handler gets 3 arguments: the data object received from the server, the text status string and the jqXHR object. However, you can still call the default framework functionality calling `this.success(...)` inside your function. +`error` | a callback function execute in case of an error. By default the alert message is displayed. If this option is overridden the alert message won't be displayed. The handler gets 3 parameters: the jqXHR object, the text status string and the error object - see [jQuery AJAX function](http://api.jquery.com/jQuery.ajax/). +`complete` | a callback function execute in case of a success or an error. +`form` | a form element to use for sourcing the form data sent with the request, either passed as a selector string or a form element. +`flash` | when true, instructs the server to clear and send any flash messages with the response. default: false +`files` | when true, the request will accept file uploads, this requires `FormData` interface support by the browser. default: false +`browserValidate` | when true, browser-based client side validation will be performed on the request before submitting. This only applies to requests triggered in the context of a `` element. **NOTE:** This form of validation does not play nice with complex forms where validated fields might not be visible to the user 100% of the time. Recommend that you avoid using it on anything but the most simple forms. +`loading` | an optional string or object to be displayed when a request runs. The string should be a CSS selector for an element, the object should support the `show()` and `hide()` functions to manage the visibility. You may pass the global object `$.wn.stripeLoadIndicator` when using the [framework extras](../ajax/extras). 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. -**handleErrorMessage(message)** | called when an error message should be displayed. -**handleValidationMessage(message, fields)** | focuses the first invalid field when validation is used. -**handleFlashMessage(message, type)** | called when a flash message is provided using the **flash** option (see above). -**handleRedirectResponse(url)** | called when the browser should redirect to another location. +`handleConfirmMessage(message)` | called when requesting confirmation from the user. +`handleErrorMessage(message)` | called when an error message should be displayed. +`handleValidationMessage(message, fields)` | focuses the first invalid field when validation is used. +`handleFlashMessage(message, type)` | called when a flash message is provided using the **flash** option (see above). +`handleRedirectResponse(url)` | called when the browser should redirect to another location. ## Usage examples 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. -**ajaxBeforeUpdate** | triggered on the form object directly after the request is complete, but before the page is updated. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. -**ajaxUpdate** | triggered on a page element after it has been updated with the framework. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. -**ajaxUpdateComplete** | triggered on the window object after all elements are updated by the framework. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. -**ajaxSuccess** | triggered on the form object after the request is successfully completed. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. -**ajaxError** | triggered on the form object if the request encounters an error. The handler gets 5 parameters: the event object, the context object, the error message, the status text string, and the jqXHR object. -**ajaxErrorMessage** | triggered on the window object if the request encounters an error. The handler gets 2 parameters: the event object and error message returned from the server. -**ajaxConfirmMessage** | triggered on the window object when `confirm` option is given. The handler gets 2 parameters: the event object and text message assigned to the handler as part of `confirm` option. This is useful for implementing custom confirm logic/interface instead of native javascript confirm box. +`ajaxBeforeSend` | triggered on the window object before sending the request. +`ajaxBeforeUpdate` | triggered on the form object directly after the request is complete, but before the page is updated. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. +`ajaxUpdate` | triggered on a page element after it has been updated with the framework. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. +`ajaxUpdateComplete` | triggered on the window object after all elements are updated by the framework. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. +`ajaxSuccess` | triggered on the form object after the request is successfully completed. The handler gets 5 parameters: the event object, the context object, the data object received from the server, the status text string, and the jqXHR object. +`ajaxError` | triggered on the form object if the request encounters an error. The handler gets 5 parameters: the event object, the context object, the error message, the status text string, and the jqXHR object. +`ajaxErrorMessage` | triggered on the window object if the request encounters an error. The handler gets 2 parameters: the event object and error message returned from the server. +`ajaxConfirmMessage` | triggered on the window object when `confirm` option is given. The handler gets 2 parameters: the event object and text message assigned to the handler as part of `confirm` option. This is useful for implementing custom confirm logic/interface instead of native javascript confirm box. These events are fired on the triggering element: Event | Description ------------- | ------------- -**ajaxSetup** | triggered before the request is formed, allowing options to be modified via the `context.options` object. -**ajaxPromise** | triggered directly before the AJAX request is sent. -**ajaxFail** | triggered finally if the AJAX request fails. -**ajaxDone** | triggered finally if the AJAX request was successful. -**ajaxAlways** | triggered regardless if the AJAX request fails or was successful. +`ajaxSetup` | triggered before the request is formed, allowing options to be modified via the `context.options` object. +`ajaxPromise` | triggered directly before the AJAX request is sent. +`ajaxFail` | triggered finally if the AJAX request fails. +`ajaxDone` | triggered finally if the AJAX request was successful. +`ajaxAlways` | triggered regardless if the AJAX request fails or was successful. ## Usage examples 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..38a81a8c 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' +``` +mypartial: '#myDiv' +``` Multiple partials are separated by commas. - firstpartial: '#myDiv', secondpartial: '#otherDiv' +``` +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' +``` +'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') - +``` +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' - +``` +'folder/append': '@#myDiv' +``` If the selector string is prepended with the `^` symbol, the content will be prepended instead. - - 'folder/append': '^#myDiv' - +``` +'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 }} +``` diff --git a/backend-controllers-ajax.md b/backend-controllers-ajax.md index 33c1f015..a0e29700 100644 --- a/backend-controllers-ajax.md +++ b/backend-controllers-ajax.md @@ -18,31 +18,35 @@ The Winter CMS backend implements the MVC pattern. Controllers manage backend pa Each controller consists of a PHP file which resides in the the **/controllers** subdirectory of a Plugin directory. Controller views are `.htm` files that reside in the controller view directory. The controller view directory name matches the controller class name written in lowercase. The view directory can also contain controller configuration files. An example of a controller directory structure: - plugins/ - acme/ - blog/ - controllers/ - users/ <=== Controller view directory - _partial.htm <=== Controller partial file - config_form.yaml <=== Controller config file - index.htm <=== Controller view file - Users.php <=== Controller class - Plugin.php +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ blog + ┣ πŸ“‚ controllers + ┃ ┣ πŸ“‚ users <=== Controller view directory + ┃ ┃ ┣ πŸ“œ _partial.htm <=== Controller partial file + ┃ ┃ ┣ πŸ“œ config_form.yaml <=== Controller config file + ┃ ┃ β”— πŸ“œ index.htm <=== Controller view file + ┃ β”— πŸ“œ Users.php <=== Controller class + β”— πŸ“œ Plugin.php +``` ### Class definition 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. @@ -51,69 +55,91 @@ Usually each controller implements functionality for working with a single type The backend controller base class defines a number of properties that allow to configure the page appearance and manage the page security: + +
+ 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

+```html +

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] +``` +backend/[author name]/[plugin name]/[controller name]/[action name] +``` The above Controller results in the following: - http://example.com/backend/acme/blog/users/index +``` +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 +153,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. @@ -214,4 +244,4 @@ public function __construct() // Middleware functionality })->except('index'); } -``` \ No newline at end of file +``` 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 111f55e3..f9c798a4 100644 --- a/backend-forms.md +++ b/backend-forms.md @@ -33,241 +33,306 @@ The **Form behavior** is a controller [behavior](../services/behaviors) used for easily adding form functionality to a backend page. The behavior provides three pages called Create, Update and Preview. The Preview page is a read-only version of the Update page. When you use the form behavior you don't need to define the `create`, `update` and `preview` actions in the controller - the behavior does it for you. However you should provide the corresponding view files. -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. +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 the `\Backend\Behaviors\FormController::class` definition to the `$implement` property of the controller class. - namespace Acme\Blog\Controllers; - - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.FormController']; +```php +namespace Acme\Blog\Controllers; - public $formConfig = 'config_form.yaml'; - } +class Categories extends \Backend\Classes\Controller +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\FormController::class, + ]; +} +``` > **NOTE:** Very often the form and [list behavior](lists) are used together in a same controller. ## Configuring the form behavior -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: +The form behaviour will load its configuration in the YAML format from a `config_form.yaml` file located in the controller's [views directory](controllers-ajax/#introduction) (`plugins/myauthor/myplugin/controllers/mycontroller/config_form.yaml`) by default. + +This can be changed by overriding the `$formConfig` property on your controller to reference a different filename or a full configuration array: - # =================================== - # Form Behavior Config - # =================================== +```php +public $formConfig = 'my_custom_form_config.yaml'; +``` + +Below is an example of a typical form behavior configuration file: - name: Blog Category - form: $/acme/blog/models/post/fields.yaml - modelClass: Acme\Blog\Post +```yaml +# =================================== +# Form Behavior Config +# =================================== - create: - title: New Blog Post +name: Blog Category +form: $/acme/blog/models/post/fields.yaml +modelClass: Acme\Blog\Post - update: - title: Edit Blog Post +create: + title: New Blog Post - preview: - title: View Blog Post +update: + title: Edit 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 Form fields are defined with the YAML file. The form fields configuration is used by the form behavior for creating the form controls and binding them to the model fields. The file is placed to a subdirectory of the **models** directory of a plugin. The subdirectory name matches the model class name written in lowercase. The file name doesn't matter, but **fields.yaml** and **form_fields.yaml** are common names. Example form fields file location: - plugins/ - acme/ - blog/ - models/ <=== Plugin models directory - post/ <=== Model configuration directory - fields.yaml <=== Model form fields config file - Post.php <=== model class +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ blog + β”— πŸ“‚ models <=== Plugin models directory + ┣ πŸ“‚ post <=== Model configuration directory + ┃ β”— πŸ“œ fields.yaml <=== Model form fields config file + β”— πŸ“œ Post.php <=== model class +``` 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 For each tab definition, namely `tabs` and `secondaryTabs`, you can specify these options: + +
+ 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 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 +373,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 +411,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 +462,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 +648,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 +665,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 +698,61 @@ 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 +``` + +>**NOTE:** If your partial field is meant only for display and will not be providing a value to the server to be stored then it is best practice to prefix the field name with an underscore (`_`) [to prevent the FormController` behavior from attempting to process it](#prevent-field-submission) ### 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 +781,76 @@ 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,42 +861,50 @@ 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 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,92 +912,121 @@ 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 Column cells can be validated against the below types of validation. Validation should be specified as an array, with the type of validation used as a key, and an optional message specified as the `message` attrbute for that 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 +1035,135 @@ 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. Disable to return just the selected model's ID +`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 +1171,125 @@ 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 +1297,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 +``` +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 +1332,74 @@ 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 +1408,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 +1497,161 @@ 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]`. To match multiple values, the following syntax can be used: `value[somevalue][othervalue]`. ### 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 +1660,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 +1687,100 @@ 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']; - - public $formConfig = 'config_form.yaml'; - } +```php +class Categories extends \Backend\Classes\Controller +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\FormController::class, + ]; +} +``` 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 +1789,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..81fd0f92 100644 --- a/backend-import-export.md +++ b/backend-import-export.md @@ -18,25 +18,32 @@ The **Import Export behavior** is a controller [behavior](../services/behaviors) that provides features for importing and exporting data. The behavior provides two pages called Import and Export. The Import page allows a user to upload a CSV file and match the columns to the database. The Export page is the opposite and allows a user to download columns from the database as a CSV file. The behavior provides the controller actions `import()` and `export()`. -The behavior configuration is defined in two parts, each part depends on a special model class along with a list and form field definition file. To use the importing and exporting behavior you should add it to the `$implement` property of the controller class. Also, the `$importExportConfig` class property should be defined and its value should refer to the YAML file used for configuring the behavior options. +The behavior configuration is defined in two parts, each part depends on a special model class along with a list and form field definition file. In order to use the Import Export behavior you should add the `\Backend\Behaviors\ImportExportController::class` definition to the `$implement` property of the controller class. ```php class Products extends Controller { + /** + * @var array List of behaviors implemented by this controller + */ public $implement = [ - 'Backend.Behaviors.ImportExportController', + \Backend\Behaviors\ImportExportController::class, ]; - - public $importExportConfig = 'config_import_export.yaml'; - - // [...] } ``` ## Configuring the behavior -The configuration file referred in the `$importExportConfig` 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: +The Import Export behaviour will load its configuration in the YAML format from a `config_import_export.yaml` file located in the controller's [views directory](controllers-ajax/#introduction) (`plugins/myauthor/myplugin/controllers/mycontroller/config_import_export.yaml`) by default. + +This can be changed by overriding the `$importExportConfig` property on your controller to reference a different filename or a full configuration array: + +```php +public $importExportConfig = 'my_custom_import_export_config.yaml'; +``` + +Below is an example of a typical Import Export behavior configuration file: ```yaml # =================================== @@ -56,12 +63,18 @@ export: The configuration options listed below are optional. Define them if you want the behavior to support the [Import](#import-page) or [Export](#export-page), or both. + +
+ 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 +93,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 @@ -101,14 +114,20 @@ export: 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 +146,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 +187,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']) ?> @@ -226,6 +245,12 @@ class SubscriberImport extends \Backend\Models\ImportModel The class must define a method called `importData` used for processing the imported data. The first parameter `$results` will contain an array containing the data to import. The second parameter `$sessionKey` will contain the session key used for the request. + +
+ Method | Description ------------- | ------------- `logUpdated()` | Called when a record is updated. @@ -303,7 +328,7 @@ class SubscriberImport extends \Backend\Models\ImportModel ## Integration with list behavior -There is an alternative approach to exporting data that uses the [list behavior](lists) to provide the export data. In order to use this feature you should have the `Backend.Behaviors.ListController` definition to the `$implement` field of the controller class. You do not need to use an export view and all the settings will be pulled from the list. Here is the only configuration needed: +There is an alternative approach to exporting data that uses the [list behavior](lists) to provide the export data. In order to use this feature you should have the `\Backend\Behaviors\ListController::class` definition to the `$implement` field of the controller class. You do not need to use an export view and all the settings will be pulled from the list. Here is the only configuration needed: ```yaml export: @@ -329,7 +354,13 @@ export: 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..a9a2182f 100644 --- a/backend-lists.md +++ b/backend-lists.md @@ -28,103 +28,142 @@ The **List behavior** is a controller [behavior](../services/behaviors) used for managing lists of records on a page. The behavior provides the sortable and searchable list with optional links for each of its records. The behavior provides the controller action `index`; however the list can be rendered anywhere and multiple list definitions can be used. -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. +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 the `\Backend\Behaviors\ListController::class` definition to the `$implement` property of the controller class. - namespace Acme\Blog\Controllers; - - class Categories extends \Backend\Classes\Controller - { - public $implement = ['Backend.Behaviors.ListController']; - - public $listConfig = 'list_config.yaml'; - } +```php +namespace Acme\Blog\Controllers; + +class Categories extends \Backend\Classes\Controller +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\ListController::class, + ]; +} +``` -> **NOTE:** Very often the list and [form behavior](../ui/form) are used together in a same controller. +> **NOTE:** Very often the list and [form behaviors](form) are used together in a same controller. ## Configuring the list behavior -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: +The List behaviour will load its configuration in the YAML format from a `config_list.yaml` file located in the controller's [views directory](controllers-ajax/#introduction) (`plugins/myauthor/myplugin/controllers/mycontroller/config_list.yaml`) by default. + +This can be changed by overriding the `$listConfig` property on your controller to reference a different filename or a full configuration array: - # =================================== - # List Behavior Config - # =================================== +```php +public $listConfig = 'my_custom_list_config.yaml'; +``` + +Below is an example of a typical List behavior configuration file: + +```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 -
+```php +
+ 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. @@ -133,47 +172,57 @@ The **filter** option should make reference to a [filter configuration file](#li List columns are defined with the YAML file. The column configuration is used by the list behavior for creating the record table and displaying model columns in the table cells. The file is placed to a subdirectory of the **models** directory of a plugin. The subdirectory name matches the model class name written in lowercase. The file name doesn't matter, but the **columns.yaml** and **list_columns.yaml** are common names. Example list columns file location: - plugins/ - acme/ - blog/ - models/ <=== Plugin models directory - post/ <=== Model configuration directory - list_columns.yaml <=== Model list columns config file - Post.php <=== model class +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ blog + β”— πŸ“‚ models <=== Plugin models directory + ┣ πŸ“‚ post <=== Model configuration directory + ┃ β”— πŸ“œ list_columns.yaml <=== Model list columns config file + β”— πŸ“œ Post.php <=== model class +``` 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 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 +259,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 +298,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 +320,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,224 +362,264 @@ 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 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. +> **NOTE:** Only scope dependencies with `type: group` are supported at this point. ### Available scope types @@ -546,41 +653,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 +704,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 +780,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 +803,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 +844,116 @@ 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']; - - public $listConfig = 'list_config.yaml'; - } +```php +class Categories extends \Backend\Classes\Controller +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\ListController::class, + ]; +} +``` 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 +962,160 @@ 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'); } +} +``` + +>**NOTE:** In order to apply the `limit()` scope to the query you have to disable the default pagination behavior of the `ListController` to prevent it from overriding any changes to the query `LIMIT`. This can be done by setting `recordsPerPage: 0` in your list definition configuration. ### 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..be8ec309 100644 --- a/backend-relations.md +++ b/backend-relations.md @@ -17,109 +17,148 @@ The **Relation behavior** is a controller [behavior](../services/behaviors) used for easily managing complex [model](../database/model) relationships on a page. It is not to be confused with [List relation columns](lists#column-types) or [Form relation fields](forms#widget-relation) that only provide simple management. -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). +The Relation behavior depends on [relation definitions](#relation-definitions). In order to use the relation behavior you should add the `\Backend\Behaviors\RelationController::class` definition to the `$implement` property of the controller class. - 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 +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\FormController::class, + \Backend\Behaviors\RelationController::class, + ]; +} +``` - public $formConfig = 'config_form.yaml'; - public $relationConfig = 'config_relation.yaml'; - } - -> **NOTE:** Very often the relation behavior is used together with the [form behavior](forms). +> **NOTE:** The relation behavior is frequently used together with the [form behavior](forms). ## Configuring the relation behavior -The configuration file referred in the `$relationConfig` property is defined in YAML format. The file should be placed into the controller's [views directory](controllers-ajax/#introduction). The required configuration depends on the [relationship type](#relationship-types) between the target model and the related model. +The relation behaviour will load its configuration in the YAML format from a `config_relation.yaml` file located in the controller's [views directory](controllers-ajax/#introduction) (`plugins/myauthor/myplugin/controllers/mycontroller/config_relation.yaml`) by default. + +This can be changed by overriding the `$relationConfig` property on your controller to reference a different filename or a full configuration array: + +```php +public $relationConfig = 'my_custom_relation_config.yaml'; +``` + +The required configuration depends on the [relationship type](#relationship-types) between the target model and the related model. 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 +183,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 +209,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 +234,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 +289,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 +316,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 +374,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 +396,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 +486,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..a0cacdb4 100644 --- a/backend-reorder.md +++ b/backend-reorder.md @@ -18,60 +18,78 @@ The behavior depends on a [model class](../database/model) which must implement > **NOTE**: If adding sorting to a previously unsorted model under the control of a third party is desired, you can use the [`Winter\Storm\Database\Behaviors\Sortable`](../database/behaviors#sortable) behavior, which can be dynamically implemented. However, you will need to ensure that the model table has a `sort_order` column present on it. -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; - - class Categories extends Controller - { - public $implement = [ - 'Backend.Behaviors.ReorderController', - ]; - - public $reorderConfig = 'config_reorder.yaml'; - - // [...] - } +In order to use the Reorder behavior you should add the `\Backend\Behaviors\ReorderController::class` definition to the `$implement` property of the controller class. + +```php +namespace Acme\Shop\Controllers; + +class Categories extends Controller +{ + /** + * @var array List of behaviors implemented by this controller + */ + public $implement = [ + \Backend\Behaviors\ReorderController::class, + ]; +} +``` ## 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: +The Reorder behaviour will load its configuration in the YAML format from a `config_reorder.yaml` file located in the controller's [views directory](controllers-ajax/#introduction) (`plugins/myauthor/myplugin/controllers/mycontroller/config_reorder.yaml`) by default. - # =================================== - # Reorder Behavior Config - # =================================== +This can be changed by overriding the `$reorderConfig` property on your controller to reference a different filename or a full configuration array: - # Reorder Title - title: Reorder Categories +```php +public $reorderConfig = 'my_custom_reorder_config.yaml'; +``` - # Attribute name - nameFrom: title +Below is an example of a typical Reorder behavior configuration file: - # Model Class name - modelClass: Acme\Shop\Models\Category +```yaml +# =================================== +# Reorder Behavior Config +# =================================== - # Toolbar widget configuration - toolbar: - # Partial for toolbar buttons - buttons: reorder_toolbar +# Reorder Title +title: Reorder Categories +# Attribute name +nameFrom: title + +# 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 +110,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..70728857 100644 --- a/backend-users.md +++ b/backend-users.md @@ -32,94 +32,102 @@ 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 +``` +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, - ], - // ... - ]; - } - -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_posts' => [ + 'label' => 'Manage the blog posts', + 'tab' => 'Blog', + 'order' => 200, + 'roles' => [\Backend\Models\UserRole::CODE_DEVELOPER, \Backend\Models\UserRole::CODE_PUBLISHER], + ], + // ... + ]; +} +``` + +When developing a plugin that will be used by more projects than just your own (i.e. published on the marketplace) it is highly recommended that you populate the `roles` property with either `\Backend\Models\UserRole::CODE_DEVELOPER` or `\Backend\Models\UserRole::CODE_PUBLISHER` or both depending on the level of access you would like users with those default system-provided roles to have to your plugin's permissions. + +You can also provide the API codes (`$role->code`) of non-default roles, but note that doing that will automatically convert that role into a "system" role which means that only the permissions that are explicitly registered to it in code will be attached to it. System roles cannot have their permissions be edited through the backend interface or in the database. Avoid attaching your permissions to non-default system roles (i.e. `CODE_DEVELOPER` & `CODE_PUBLISHER`) if you will be publishing the plugin on the marketplace or otherwise making it available for use in other projects that may not have your custom roles defined (unless your plugin itself provides said custom role via a seeder run during the migration process). + +>**NOTE:** If the `roles` property isn't provided then the only users that will have access to the permission by default will be superusers or users with the `\Backend\Models\UserRole::CODE_DEVELOPER` role which inherits all "orphaned permissions" (permissions without any roles specified). ## 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 +136,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')): ?> - - +```php +user->hasAccess('acme.blog.delete_categories')): ?> + + +``` diff --git a/backend-views-partials.md b/backend-views-partials.md index 8c65cb64..7cea7b80 100644 --- a/backend-views-partials.md +++ b/backend-views-partials.md @@ -11,59 +11,77 @@ 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 +90,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..d6490f2f 100644 --- a/backend-widgets.md +++ b/backend-widgets.md @@ -24,90 +24,108 @@ Widgets are the backend equivalent of frontend [Components](../cms/components). Widget classes reside inside the **widgets** directory of the plugin directory. The directory name matches the name of the widget class written in lowercase. Widgets can supply assets and partials. An example widget directory structure looks like this: - widgets/ - /form - /partials - _form.htm <=== Widget partial file - /assets - /js - form.js <=== Widget JavaScript file - /css - form.css <=== Widget StyleSheet file - Form.php <=== Widget class +```css +πŸ“‚ widgets + ┣ πŸ“‚ form + ┃ ┣ πŸ“‚ partials + ┃ ┃ β”— πŸ“œ _form.htm <=== Widget partial file + ┃ β”— πŸ“‚ assets + ┃ ┣ πŸ“‚ js + ┃ ┃ β”— πŸ“œ form.js <=== Widget JavaScript file + ┃ β”— πŸ“‚ css + ┃ β”— πŸ“œ form.css <=== Widget StyleSheet file + β”— πŸ“œ Form.php <=== Widget class +``` ### Class definition 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" +``` +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 @@ -116,109 +134,119 @@ With form widgets you can add new control types to the backend [forms](../backen Form Widget classes reside inside the **formwidgets** directory of the plugin directory. The directory name matches the name of the widget class written in lowercase. Widgets can supply assets and partials. An example form widget directory structure looks like this: - formwidgets/ - /form - /partials - _form.htm <=== Widget partial file - /assets - /js - form.js <=== Widget JavaScript file - /css - form.css <=== Widget StyleSheet file - Form.php <=== Widget class +```css +πŸ“‚ formwidgets + ┣ πŸ“‚ form + ┃ ┣ πŸ“‚ partials + ┃ ┃ β”— πŸ“œ _form.htm <=== Widget partial file + ┃ β”— πŸ“‚ assets + ┃ ┣ πŸ“‚ js + ┃ ┃ β”— πŸ“œ form.js <=== Widget JavaScript file + ┃ β”— πŸ“‚ css + ┃ β”— πŸ“œ form.css <=== Widget StyleSheet file + β”— πŸ“œ Form.php <=== Widget class +``` ### Class definition 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 +255,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 @@ -269,87 +305,95 @@ Report widgets can be used on the backend dashboard and in other backend report The report widget classes should extend the `Backend\Classes\ReportWidgetBase` class. As any other plugin class, generic widget controllers should belong to the [plugin namespace](../plugin/registration#namespaces). The class should override the `render` method in order to render the widget itself. Similarly to all backend widgets, report widgets use partials and a special directory layout. Example directory layout: - plugins/ - winter/ <=== Author name - googleanalytics/ <=== Plugin name - reportwidgets/ <=== Report widgets directory - trafficsources <=== Widget files directory - partials - _widget.htm - TrafficSources.php <=== Widget class file +```css +πŸ“‚ plugins + β”— πŸ“‚ winter <=== Author name + β”— πŸ“‚ googleanalytics <=== Plugin name + β”— πŸ“‚ reportwidgets <=== Report widgets directory + ┣ πŸ“‚ trafficsources <=== Widget files directory + ┃ β”— πŸ“‚ partials + ┃ β”— πŸ“œ _widget.htm + β”— πŸ“œ TrafficSources.php <=== Widget class file +``` 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 +404,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. 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..d6219a88 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/*" +``` +"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 @@ -131,52 +139,62 @@ Create **media** folder in the container. The folder name doesn't matter. This f You should create an API user that Winter CMS will use for managing files in the CDN container. Open Account / User Management page in Rackspace console. Click **Create user** button. Fill in the user name (for example winter.cdn.api), password, security question and answer. In the **Product Access** section select **Custom** and in the CDN row select **Admin**. Use **No Access** role in the **Account** section and use **Technical Contact** type in the **Contact Information** section. Save the user account. After saving the account you will see the Login Details section with the **API Key** row that contains a value you need to use in Winter CMS configuration files. -Now you have all the information to update Winter CMS configuration. Open **config/filesystem.php** script and find the **disks** section. It already contains Rackspace configuration, you need to replace the API credentials and container information parameters: +Now you have all the information to update Winter CMS configuration. Open **config/filesystems.php** script and find the **disks** section. It already contains Rackspace configuration, you need to replace the API credentials and container information parameters: + + +
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,81 +203,106 @@ 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' => [ - ... +```php +'storage' => [ + ... - 'media' => [ - ... - 'ignore' => ['.svn', '.git', '.DS_Store'] - ] - ], + '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 The Media Manager provides a few [events](../services/events) that you can listen for in order to improve extensibility. + +
+ 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..ce0b69a2 100644 --- a/cms-pages.md +++ b/cms-pages.md @@ -17,73 +17,99 @@ 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 Page configuration is defined in the [Configuration Section](themes#configuration-section) of the page template file. The page configuration defines the page parameters, required for the routing and rendering the page and its [Components](../cms/components), which are explained in another article. The following configuration parameters are supported for pages: + +
+ 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 +130,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 +169,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 +213,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 +232,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..433cc7a6 100644 --- a/cms-themes.md +++ b/cms-themes.md @@ -32,39 +32,39 @@ Object | Description Below, you can see an example theme directory structure. Each Winter theme is represented with a separate directory and generally, one active theme is used for displaying the website. This example displays the "example-theme" theme directory. -``` +```css πŸ“‚ themes - ┣ πŸ“‚ example-theme - ┃ ┣ πŸ“‚ assets <-- Theme assets - ┃ ┃ ┣ πŸ“‚ css - ┃ ┃ ┣ πŸ“‚ fonts - ┃ ┃ ┣ πŸ“‚ images - ┃ ┃ ┣ πŸ“‚ javascript - ┃ ┃ ┣ πŸ“‚ scss - ┃ ┃ β”— πŸ“‚ vendor - ┃ ┣ πŸ“‚ content <-- Static content files - ┃ ┃ β”— πŸ“‚ static-pages <-- Content files from the Winter.Pages plugin - ┃ ┃ ┃ β”— πŸ“œ index.htm - ┃ ┃ β”— πŸ“œ welcome.htm - ┃ ┣ πŸ“‚ layouts <-- Theme Layouts (Page scaffolds / templates) - ┃ ┃ ┣ πŸ“œ default.htm - ┃ ┃ β”— πŸ“œ sidebar.htm - ┃ ┣ πŸ“‚ meta <-- Menu definitions and other plugin specific YAML files - ┃ ┃ ┣ πŸ“‚ menus - ┃ ┃ ┃ ┣ πŸ“œ main-menu.yaml - ┃ ┃ ┃ β”— πŸ“œ sitemap.yaml <-- Meta file describing the sitemap - ┃ ┃ β”— πŸ“œ static-pages.yaml <-- Meta file describing the structure of the Winter.Pages static pages - ┃ ┣ πŸ“‚ pages <-- Theme Pages (Contain the routing for the frontend) - ┃ ┃ ┣ πŸ“œ 404.htm <-- Page for 404 responses - ┃ ┃ ┣ πŸ“œ home.htm - ┃ ┃ ┣ πŸ“œ error.htm <-- Page for 500 responses - ┃ ┃ β”— πŸ“œ sitemap.htm <-- Page for rendering the sitemap response - ┃ ┣ πŸ“‚ partials <-- Theme Partials (Reusable pieces of HTML markup) - ┃ ┃ ┣ πŸ“œ html-footer.htm - ┃ ┃ ┣ πŸ“œ html-header.htm - ┃ ┃ β”— πŸ“œ navbar.htm - ┃ ┣ πŸ“œ theme.yaml <-- Theme information file - ┃ β”— πŸ“œ version.yaml <-- Theme updates file + β”— πŸ“‚ example-theme + ┣ πŸ“‚ assets <-- Theme assets + ┃ ┣ πŸ“‚ css + ┃ ┣ πŸ“‚ fonts + ┃ ┣ πŸ“‚ images + ┃ ┣ πŸ“‚ javascript + ┃ ┣ πŸ“‚ scss + ┃ β”— πŸ“‚ vendor + ┣ πŸ“‚ content <-- Static content files + ┃ ┣ πŸ“‚ static-pages <-- Content files from the Winter.Pages plugin + ┃ ┃ β”— πŸ“œ index.htm + ┃ β”— πŸ“œ welcome.htm + ┣ πŸ“‚ layouts <-- Theme Layouts (Page scaffolds / templates) + ┃ ┣ πŸ“œ default.htm + ┃ β”— πŸ“œ sidebar.htm + ┣ πŸ“‚ meta <-- Menu definitions and other plugin specific YAML files + ┃ ┣ πŸ“‚ menus + ┃ ┃ ┣ πŸ“œ main-menu.yaml + ┃ ┃ β”— πŸ“œ sitemap.yaml <-- Meta file describing the sitemap + ┃ β”— πŸ“œ static-pages.yaml <-- Meta file describing the structure of the Winter.Pages static pages + ┣ πŸ“‚ pages <-- Theme Pages (Contain the routing for the frontend) + ┃ ┣ πŸ“œ 404.htm <-- Page for 404 responses + ┃ ┣ πŸ“œ home.htm + ┃ ┣ πŸ“œ error.htm <-- Page for 500 responses + ┃ β”— πŸ“œ sitemap.htm <-- Page for rendering the sitemap response + ┣ πŸ“‚ partials <-- Theme Partials (Reusable pieces of HTML markup) + ┃ ┣ πŸ“œ html-footer.htm + ┃ ┣ πŸ“œ html-header.htm + ┃ β”— πŸ“œ navbar.htm + ┣ πŸ“œ theme.yaml <-- Theme information file + β”— πŸ“œ version.yaml <-- Theme updates file ``` > The active theme is set with the `activeTheme` parameter in the `config/cms.php` file or with the Theme Selector on the System > CMS > Frontend Theme backend page. The theme set with the Theme Selector overrides the value in the `config/cms.php` file. @@ -74,26 +74,29 @@ Below, you can see an example theme directory structure. Each Winter theme is re Winter supports single level subdirectories for **pages**, **partials**, **layouts** and **content** files, while the **assets** directory can have any structure. This simplifies the organization of large websites. In the example directory structure below, you can see that the **pages** and **partials** directories contain the **blog** subdirectory, and the **content** directory contains the **home** subdirectory. - themes/ - website/ - pages/ - home.htm - blog/ <=== Subdirectory - archive.htm - category.htm - partials/ - sidebar.htm - blog/ <=== Subdirectory - category-list.htm - content/ - footer-contacts.txt - home/ <=== Subdirectory - intro.htm - ... +```css +πŸ“‚ themes + β”— πŸ“‚ website + ┣ πŸ“‚ pages + ┃ ┣ πŸ“œhome.htm + ┃ β”— πŸ“‚ blog <=== Subdirectory + ┃ ┣ πŸ“œ archive.htm + ┃ β”— πŸ“œ category.htm + ┣ πŸ“‚ partials + ┃ ┣ πŸ“œ sidebar.htm + ┃ β”— πŸ“‚ blog <=== Subdirectory + ┃ β”— πŸ“œ category-list.htm + β”— πŸ“‚ content + ┣ πŸ“œ footer-contacts.txt + β”— πŸ“‚ home <=== Subdirectory + β”— πŸ“œ intro.htm +``` 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 +107,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 @@ -197,7 +210,7 @@ The theme changelog can be viewed at **Settings -> Theme log**. Each change has ## Database Driven Themes -Winter CMS comes with another very useful feature, disabled by default, called Database Driven Themes. When this feature is enabled (by setting `cms.databaseTemplates` to `true`, or `null` when `app.debug` is `false`); the database layer stores all modified CMS files in the database. Files that are not modified continue to be loaded from the filesystem. There is a [`theme:sync $themeDir`](../console/commands#theme-sync-command) console command that can be used to sync changes between the filesystem and database. +Winter CMS comes with another very useful feature, disabled by default, called Database Driven Themes. When this feature is enabled (by setting `cms.databaseTemplates` to `true`, or `null` when `app.debug` is `false`); the database layer stores all modified CMS files in the database. Files that are not modified continue to be loaded from the filesystem. There is a [`theme:sync $themeDir`](../console/theme-management#theme-sync) console command that can be used to sync changes between the filesystem and database. Files modified in the database are cached to indicate that they should be loaded from the database. diff --git a/config/toc-docs.yaml b/config/toc-docs.yaml index 23619f9a..842efdbc 100644 --- a/config/toc-docs.yaml +++ b/config/toc-docs.yaml @@ -66,6 +66,18 @@ Plugins: ajax/javascript-api: "JavaScript API" ajax/extras: "Extra Features" +Snowboard: + icon: "icon-bolt" + pages: + snowboard/introduction: "Introduction" + snowboard/migration-guide: "Migration Guide" + snowboard/handlers: "Serverside Event Handlers" + snowboard/request: "AJAX Requests (JavaScript API)" + snowboard/data-attributes: "AJAX Requests (Data Attributes API)" + snowboard/extras: "Extra Features" + snowboard/utilities: "Utilities" + snowboard/plugin-development: "Plugin Development" + Database: icon: "icon-hdd" pages: @@ -109,9 +121,13 @@ Services: Console: icon: "icon-terminal" pages: - console/commands: "Command List" + console/introduction: "Introduction" + console/setup-maintenance: "Setup & Maintenance" + console/plugin-management: "Plugin Management" + console/theme-management: "Theme Management" + console/asset-compilation: "Asset Compilation (Mix)" console/scaffolding: "Scaffolding" - console/development: "Development" + console/utilities: "Utilities" Events: pages: diff --git a/config/toc-markup.yaml b/config/toc-markup.yaml index 0d542f0e..ff321ef4 100644 --- a/config/toc-markup.yaml +++ b/config/toc-markup.yaml @@ -2,6 +2,7 @@ Markup: icon: "icon-code" pages: markup/templating: "Templating" + "https://twig.symfony.com/doc/2.x/": "Twig Docs@" Variables: icon: "icon-subscript" @@ -37,21 +38,22 @@ Filters: itemClass: "code-item" pages: markup/filter-app: "| app" - markup/filter-page: "| page" - markup/filter-theme: "| theme" - markup/filter-media: "| media" - markup/filter-resize: "| resize" + markup/filter-asset: "| asset" + markup/filter-default: "| default" markup/filter-image-width: "| imageWidth" markup/filter-image-height: "| imageHeight" - markup/filter-md: "| md" + markup/filter-md: "| md[_line|_safe]" + markup/filter-media: "| media" + markup/filter-page: "| page" markup/filter-raw: "| raw" - markup/filter-default: "| default" + markup/filter-resize: "| resize" + markup/filter-theme: "| theme" Functions: icon: "icon-eraser" itemClass: "code-item" pages: - markup/function-str: "str()" - markup/function-form: "form()" + markup/function-str: "str_*()" + markup/function-form: "form_*()" markup/function-html: "html()" markup/function-dump: "dump()" diff --git a/console-asset-compilation.md b/console-asset-compilation.md new file mode 100644 index 00000000..1a296c4b --- /dev/null +++ b/console-asset-compilation.md @@ -0,0 +1,207 @@ +# Asset Compilation (Mix) + +- [Introduction](#introduction) +- [Requirements](#requirements) +- [Registering a package](#registering-packages) + - [Automatic registration](#automatic-registration) + - [Registering plugin packages](#registering-plugins) + - [Registering theme packages](#registering-themes) +- [Mix configuration](#mix-configuration) +- [Examples](#examples) +- [Commands](#commands) + - [Install Node dependencies](#mix-install) + - [List registered Mix packages](#mix-list) + - [Compile a Mix package](#mix-compile) + - [Watch a Mix package](#mix-watch) + + +## Introduction + +Winter brings first-class support for handling Node-based compilation for frontend assets through the Mix commands. The comamnds use the [Laravel Mix](https://laravel-mix.com/) wrapper, a user-friendly and simple interface for setting up compilation of multiple types of frontend assets through Webpack and various libraries. + + +### Requirements + +To take advantage of Mix asset compilation, you must have Node and the Node package manager (NPM) installed in your development environment. This will be dependent on your operating system - please review the [Download NodeJS](https://nodejs.org/en/download/) page for more information on installing Node. + +[Laravel Mix](https://laravel-mix.com/) should also be present in the `package.json` file for any packages that will be using it (either in `dependencies` or a `devDependencies`) but if it is not specified in the project's `package.json` file then it can be optionally automatically added when running the [`mix:install`](#mix-install) command. + + +## Registering a package + +Registering for asset compilation through Mix is very easy. Automatic registration should meet your needs most of the time, and if not there are several methods available to manually register Mix packages. + + +### Automatic registration + +By default, Winter will scan all available and enabled modules, plugins and themes for the presence of a `winter.mix.js` file under each extension's root folder (i.e. `modules/system/winter.mix.js`, `plugins/myauthor/myplugin/winter.mix.js`, or `themes/mytheme/winter.mix.js`). + +If the `winter.mix.js` file is found, it will be automatically registered as a package with an automatically generated package name, and will show up when running the Mix commands. Most of the time, this should be all you need to do in order to get started with Laravel Mix asset compilation in Winter CMS. + + +### Registering plugins + +To register frontend assets to be compiled through Mix in your plugin, simply return an array with the package names as the keys and the package paths relative to the plugin's directory as the values to register from your [`Plugin.php`](../plugin/registration) registration file's `registerMixPackages()` method. See below example. + +```php +public function registerMixPackages() +{ + return [ + 'custom-package-name' => 'assets/js/build.mix.js', + ]; +} +``` + + +### Registering themes + +Registration of asset compilation of themes is even easier, and can be done by adding a `mix` definition to your [theme information file](../themes/development#theme-information) (`theme.yaml`). + +```yaml +name: "Winter CMS Demo" +description: "Demonstrates the basic concepts of the frontend theming." +author: "Winter CMS" +homepage: "https://wintercms.com" +code: "demo" + +mix: + : winter.mix.js +``` + +The `mix` definition takes any number of registered packages as a YAML object, with the key being the name of the package as a kebab-case string and the location of your `winter.mix.js` file relative to the theme's root directory. + +For example, if you want to register two packages called `demo-theme-style` and `demo-theme-shop` located in the assets folder, you would use the following definition: + +```yaml +name: "Winter CMS Demo" +description: "Demonstrates the basic concepts of the frontend theming." +author: "Winter CMS" +homepage: "https://wintercms.com" +code: "demo" + +mix: + demo-theme-style: assets/style/winter.mix.js + demo-theme-shop: assets/shop/winter.mix.js +``` + + +## Mix configuration + +The Mix configuration file (`winter.mix.js`) is a configuration file that manages the configuration of Laravel Mix itself. In conjunction with the `package.json` file that defines your dependencies, this file defines how Laravel Mix will compile your assets. + +You can [review examples](https://laravel-mix.com/docs/6.0/examples) or the [full Mix API](https://laravel-mix.com/docs/6.0/api) at the [Laravel Mix website](https://laravel-mix.com). + +Your `winter.mix.js` file must include Mix as a requirement, and must also define the public path to the current directory, as follows: + +```js +const mix = require('laravel-mix'); + +// For assets in the current directory +// mix.setPublicPath(__dirname); + +// For assets in a /assets subdirectory +mix.setPublicPath(__dirname + '/assets'); + +// Your mix configuration below +``` + +### Paths + +When the `winter.mix.js` file is evaluated, regardless of where you ran `mix:compile` from, the working directory is set to the parent directory of the `winter.mix.js` file. That means that any relative paths used in the configuration will be relative to the current directory of the `winter.mix.js` file. + +>**NOTE:** Winter's [path symbols](../services/helpers#path-symbols) are also supported in the `winter.mix.js` file. + + +## Examples + +Here are some examples of installing common frontend libraries for use with the asset compilation. + +### Tailwind CSS + +For themes that wish to use Tailwind CSS, include the `tailwindcss`, `postcss` and `autoprefixer` dependencies in your `package.json` file. + +```bash +# Inside the project root folder +npm install --save-dev tailwindcss postcss autoprefixer + +# Run the Tailwind initialisation +npx taildwindcss init +``` + +This will create a Tailwind configuration file (`tailwind.config.js`) inside your theme that you may [configure](https://tailwindcss.com/docs/installation) to your specific theme's needs. + +Then, add a `winter.mix.js` configuration file that will compile Tailwind as needed: + +```js +const mix = require('laravel-mix'); +mix.setPublicPath(__dirname); + +// Render Tailwind style +mix.postCss('assets/css/base.css', 'assets/css/theme.css', [ + require('postcss-import'), + require('tailwindcss'), +]); +``` + +In the example above, we have a base CSS file that contains the Tailwind styling - `assets/css/base.css` - that will compile to a final compiled CSS file in `assets/css/theme.css`. + +Your theme will now be ready for Tailwind CSS development. + + +## Commands + + +### Install Node dependencies + +```bash +php artisan mix:install [-p ] [--npm ] +``` + +The `mix:install` command will install Node dependencies for all registered Mix packages. + +This command will add each registered package to the `workspaces.packages` property of your root `package.json` file and then run and display the results of `npm install` from your project root to install all of the dependencies for all of the registered packages at once. + +You can optionally provide a `-p` or `--package` flag to install dependencies for one or more packages. To define multiple packages, simply add more `-p` flags to the end of the command. + +If the command is run with a `-p` or `--package` flag and the provided package name is not already registered and the name matches a valid module, plugin, or theme package name (modules are prefixed with `module-$moduleDirectory`, themes are prefixed with `theme-$themeDirectory`, and plugins are simply `Author.Plugin`) then a `winter.mix.js` file will be automatically generated for that package and will be included in future runs of any mix commands through the [automatic registration](#automatic-registration) feature. + +The `--npm` flag can also be provided if you have a custom path to the `npm` program. If this is not provided, the system will try to guess where `npm` is located. + + +### List registered Mix packages + +```bash +php artisan mix:list +``` + +The `mix:list` command will list all registered Mix packages found in the Winter installation. This is useful for determining if your plugin or theme is correctly registered. + +The command will list all packages, as well as the directory for the asset and the configuration file that has been defined during registration. + + +### Compile a Mix packages + +```bash +php artisan mix:compile [-p ] [-f|--production] [-- ] +``` + +The `mix:compile` command compiles all registered Mix packages, running each package through Laravel Mix for compilation. + +By specifying the `-p` flag, you can compile one or more selected packages. To define multiple packages, simply add more `-p` flags to the end of the command. + +By default, all packages are built in "development" mode. If you wish to compile in "production" mode, which may include more optimisations for production sites, add the `-f` or `--production` flag to the command. + +The command will generate a report of all compiled files and their final size once complete. + +If you wish to pass extra options to the Webpack CLI, for special cases of compilation, you can add `--` to the end of the command, followed by [any parameters](https://webpack.js.org/api/cli/) as per the Webpack CLI options. + + +### Watch a Mix package + +```bash +php artisan mix:watch [-f|--production] [-- ] +``` + +The `mix:watch` command is similar to the the `mix:compile` command, except that it remains active and watches for any changes made to files that would be affected by your compilation. When any changes are made, a compile is automatically executed. This is useful for development in allowing you to quickly make changes and review them in your browser. + +With this command, only one package can be provided and watched at any one time. diff --git a/console-commands.md b/console-commands.md deleted file mode 100644 index 01c53e94..00000000 --- a/console-commands.md +++ /dev/null @@ -1,422 +0,0 @@ -# Command Line Interface - -- [Console installation](#console-install) - - [Quick start install](#console-install-quick) - - [Composer install](#console-install-composer) -- [Setup & Maintenance](#maintenance-commands) - - [Install command](#console-install-command) - - [System update](#console-update-command) - - [Database migration](#console-up-command) - - [Change backend user password](#change-backend-user-password-command) -- [Plugin management](#plugin-commands) - - [Install plugin](#plugin-install-command) - - [Refresh plugin](#plugin-refresh-command) - - [Rollback plugin](#plugin-rollback-command) - - [List plugin](#plugin-list-command) - - [Disable plugin](#plugin-disable-command) - - [Enable plugin](#plugin-enable-command) - - [Remove plugin](#plugin-remove-command) -- [Theme management](#theme-commands) - - [Install theme](#theme-install-command) - - [List themes](#theme-list-command) - - [Enable theme](#theme-use-command) - - [Remove theme](#theme-remove-command) - - [Sync theme](#theme-sync-command) -- [Utilities](#utility-commands) - - [Clear Application Cache](#cache-clear-command) - - [Remove Demo Data](#winter-fresh-command) - - [Mirror Public Directory](#cache-clear-command) - - [Enable DotEnv Configuration](#winter-env-command) - - [Run Unit Tests](#winter-test-command) - - [Miscellaneous Commands](#winter-util-command) - - [Compile Asset Bundles](#winter-util-compile-assets-command) - - [Recursively Git Pull](#winter-util-git-pull-command) - - [Purge Thumbnails](#winter-util-purge-thumbs-command) - - [Purge Uploaded Files](#winter-util-purge-uploads-command) - - [Purge Orphaned Files](#winter-util-purge-orphans-command) - -Winter includes several command-line interface (CLI) commands and utilities that allow to install Winter, update it, as well as speed up the development process. The console commands are based on Laravel's [Artisan](http://laravel.com/docs/artisan) tool. You may [develop your own console commands](../console/development) or speed up development with the provided [scaffolding commands](../console/scaffolding). - - -## Console installation - -Console installation can be performed using the native system or with [Composer](http://getcomposer.org/) to manage dependencies. Either approach will download the Winter application files and can be used right away. If you plan on using a database, be sure to run the [install command](#console-install-command) after installation. - - -### Composer install - -Download the application source code by using `create-project` in your terminal. The following command will install to a directory called **/mywinter**. - -```bash -composer create-project wintercms/winter mywinter -``` - -Once this task has finished, open the file **config/cms.php** and enable the `disableCoreUpdates` setting. This will disable core updates from being delivered by the Winter gateway. - -```php -return [ - // ... - - 'disableCoreUpdates' => true, - - // ... -]; -``` - -When updating Winter, use the composer update command as normal before performing a [database migration](#console-up-command). - -```bash -composer update -``` - -Composer is configured to look inside plugin directories for composer dependencies through the use of the `wikimedia-merge-plugin` and these will be included when running `composer update`. - -> **NOTE:** To use composer with a Winter instance that has been installed using the [Wizard installation](../setup/installation#wizard-installation), simply copy the `tests/` directory and `composer.json` file from [GitHub](https://github.com/wintercms/winter) into your Winter instance and then run `composer install`. - - -## Setup & Maintenance - - -### Install command - -The `winter:install` command will guide you through the process of setting up Winter CMS for the first time. It will ask for the database configuration, application URL, encryption key and administrator details. - -```bash -php artisan winter:install -``` - -You also may wish to inspect **config/app.php** and **config/cms.php** to change any additional configuration. - -> **NOTE:** You cannot run `winter:install` after running `winter:env`. `winter:env` takes the existing configuration values and puts them in the `.env` file while replacing the original values with calls to `env()` within the configuration files. `winter:install` cannot now replace those calls to `env()` within the configuration files as that would be overly complex to manage. - - -### System update - -The `winter:update` command will request updates from the Winter gateway. It will update the core application and plugin files, then perform a database migration. - -```bash -php artisan winter:update -``` - -> **IMPORTANT**: If you are using [using composer](#console-install-composer) do **NOT** run this command without first making sure that `cms.disableCoreUpdates` is set to true. Doing so will cause conflicts between the marketplace version of Winter and the version available through composer. In order to update the core Winter installation when using composer run `composer update` instead. - - -### Database migration - -The `winter:up` command will perform a database migration, creating database tables and executing seed scripts, provided by the system and [plugin version history](../plugin/updates). The migration command can be run multiple times, it will only execute a migration or seed script once, which means only new changes are applied. - -```bash -php artisan winter:up -``` - -The inverse command `winter:down` will reverse all migrations, dropping database tables and deleting data. Care should be taken when using this command. The [plugin refresh command](#plugin-refresh-command) is a useful alternative for debugging a single plugin. - -```bash -php artisan winter:down -``` - - -### Change backend user password - -The `winter:passwd` command will allow the password of a backend user or administrator to be changed via the command-line. This is useful if someone gets locked out of their Winter CMS install, or for changing the password for the default administrator account. - -```bash -php artisan winter:passwd username password -``` - -You may provide the username/email and password as both the first and second argument, or you may leave the arguments blank, in which case the command will be run interactively. - - -## Plugin management - -Winter includes a number of commands for managing plugins. - - -### Install plugin - -`plugin:install` - downloads and installs the plugin by its name. The next example will install a plugin called **AuthorName.PluginName**. Note that your installation should be bound to a project in order to use this command. You can create projects on Winter website, in the [Account / Projects](https://wintercms.com/account/project/dashboard) section. - -```bash -php artisan plugin:install AuthorName.PluginName -``` - -> **NOTE:** If you have already have the plugin files locally either through Composer or manually uploading them then you can just run [`winter:up`](#console-up-command) to run the plugin's pending migrations to "install" it. This command is mostly meant for instaling plugins sourced from the [Winter CMS Marketplace](https://wintercms.com/marketplace) - - -### Refresh plugin - -`plugin:refresh` - destroys the plugin's database tables and recreates them. This command is useful for development. - -```bash -php artisan plugin:refresh AuthorName.PluginName -``` - - -### Rollback plugin - -`plugin:rollback` - Rollback the specified plugin's migrations. The second parameter is optional, if specified the rollback process will stop at the specified version. - -```bash -php artisan plugin:rollback AuthorName.PluginName 1.2.3 -``` - - -### List Plugins - -`plugin:list` - Displays a list of installed plugins. - -```bash -php artisan plugin:list -``` - - -### Disable Plugin - -`plugin:disable` - Disable an existing plugin. - -```bash -php artisan plugin:disable AuthorName.PluginName -``` - - -### Enable Plugin - -`plugin:enable` - Enable a disabled plugin. - -```bash -php artisan plugin:enable AuthorName.PluginName -``` - - -### Remove plugin - -`plugin:remove` - destroys the plugin's database tables and deletes the plugin files from the filesystem. - -```bash -php artisan plugin:remove AuthorName.PluginName -``` - - -## Theme management - -Winter includes a number of commands for managing themes. - - -### Install theme - -`theme:install` - download and install a theme from the [Marketplace](https://wintercms.com/marketplace). The following example will install the theme in `/themes/authorname-themename` - -```bash -php artisan theme:install AuthorName.ThemeName -``` - -If you wish to install the theme in a custom directory, simply provide the second argument. The following example will download `AuthorName.ThemeName` and install it in `/themes/my-theme` - -```bash -php artisan theme:install AuthorName.ThemeName my-theme -``` - - -### List themes - -`theme:list` - list installed themes. Use the **-m** option to include popular themes in the Marketplace. - -```bash -php artisan theme:list -``` - - -### Enable theme - -`theme:use` - switch the active theme. The following example will switch to the theme in `/themes/winter-vanilla` - -```bash -php artisan theme:use winter-vanilla -``` - - -### Remove theme - -`theme:remove` - delete a theme. The following example will delete the directory `/themes/winter-vanilla` - -```bash -php artisan theme:remove winter-vanilla -``` - - -### Sync theme - -`theme:sync` - Sync a theme's content between the filesystem and database when `cms.databaseTemplates` is enabled. - -```bash -php artisan theme:sync -``` - -By default the theme that will be synced is the currently active one. You can specify any theme to sync by passing the desired theme's code: - -```bash -php artisan theme:sync my-custom-theme -``` - -By default the sync direction will be from the database to the filesytem (i.e. you're syncing changes on a remote host to the filesystem for tracking in a version control system). However, you can change the direction of the sync by specifying `--target=database`. This is useful if you have changed the underlying files that make up the theme and you want to force the site to pick up your changes even if they have made changes of their own that are stored in the database. - -```bash -php artisan theme:sync --target=database -``` - -By default the command requires user interaction to confirm that they want to complete the sync (including information about the amount of paths affected, the theme targeted, and the target & source of the sync). To override the need for user interaction (i.e. if running this command in a deploy / build script of some sort) just pass the `--force` option: - -```bash -php artisan theme:sync --force -``` - -Unless otherwise specified, the command will sync all the valid paths (determined by the Halcyon model instances returned to the `system.console.theme.sync.getAvailableModelClasses` event) available in the theme. To manually specify specific paths to be synced pass a comma separated list of paths to the `--paths` option: - -```bash -php artisan theme:sync --paths=partials/header.htm,content/contact.md -``` - - -## Utilities - -Winter includes a number of utility commands. - - -### Clear Application Cache - -`cache:clear` - clears the application, twig and combiner cache directories. Example: - -```bash -php artisan cache:clear -``` - - -### Remove Demo Data - -`winter:fresh` - removes the demo theme and plugin that ships with Winter. - -```bash -php artisan winter:fresh -``` - - -### Mirror Public Directory - -`winter:mirror` - creates a mirrored copy of the public files needed to serve the application, using symbolic linking. This command is used when [setting up a public folder](../setup/configuration#public-folder). - -```bash -php artisan winter:mirror public -``` - -> **NOTE:** By default the symlinks created will be absolute symlinks, to create them as relative symlinks instead include the `--relative` option: - -```bash -php artisan winter:mirror public --relative -``` - - -### Enable DotEnv Configuration - -`winter:env` - changes common configuration values to [DotEnv syntax](../setup/configuration#dotenv-configuration). - - php artisan winter:env - - -### Run unit tests - -Runs the unit tests for the entire project, a specific plugin, or the Winter CMS core. - -To run the entire project's unit tests: - -```bash -php artisan winter:test -``` - -Or, to run only the core unit tests, use the `-o` or `--core` option: - -```bash -php artisan winter:test -o -``` - -To run a specific plugin's tests, use the `-p` or `--plugin=` option: - -```bash -php artisan winter:test -p Acme.Demo -``` - -To run a custom test suite, use the `-c` or `--configuration=` option: - -```bash -php artisan winter:test -c ./custom-path/phpunit.xml -``` - -If using additional PHPUnit parameters / options, they must be included after the winter:test command's options: - -```bash -php artisan winter:test -p Acme.Demo --filter=FilteredTest --stop-on-failure -``` - - -### Miscellaneous commands - -`winter:util` - a generic command to perform general utility tasks, such as cleaning up files or combining files. The arguments passed to this command will determine the task used. - - -#### Compile Asset Bundles - -Outputs combined system files for JavaScript (js), StyleSheets (less), client side language (lang), or everything (assets). - -```bash -php artisan winter:util compile assets -php artisan winter:util compile lang -php artisan winter:util compile js -php artisan winter:util compile less -``` - -To combine without minification, pass the `--debug` option. - -```bash -php artisan winter:util compile js --debug -``` - - -#### Pull all repos - -This will execute the command `git pull` on all theme and plugin directories. - -```bash -php artisan winter:util git pull -``` - - -#### Purge Thumbnails - -Deletes all generated thumbnails in the uploads directory. - -```bash -php artisan winter:util purge thumbs -``` - - -#### Purge Uploaded Files - -Deletes files in the uploads directory that do not exist in the "system_files" table. - -```bash -php artisan winter:util purge uploads -``` - - -#### Purge Orphaned Files - -Deletes records in "system_files" table that do not belong to any other model. - -```bash -php artisan winter:util purge orphans -``` - -To also delete records that have no associated file in the local storage, pass the `--missing-files` option. - -```bash -php artisan winter:util purge orphans --missing-files -``` diff --git a/console-development.md b/console-development.md deleted file mode 100644 index e36fe476..00000000 --- a/console-development.md +++ /dev/null @@ -1,255 +0,0 @@ -# Console Development - -- [Introduction](#introduction) -- [Building a command](#building-a-command) - - [Defining arguments](#defining-arguments) - - [Defining options](#defining-options) - - [Writing output](#writing-output) - - [Retrieving input](#retrieving-input) -- [Registering commands](#registering-commands) -- [Calling other commands](#calling-other-commands) - - -## Introduction - -In addition to the provided console commands, you may also build your own custom commands for working with your application. You may store your custom commands within the plugin **console** directory. You can generate the class file using the [command line scaffolding tool](../console/scaffolding#scaffold-create-command). - - -## Building a command - -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!'); - } - - /** - * Get the console command arguments. - * @return array - */ - protected function getArguments() - { - 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. - - -### Defining arguments - -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.'], - ]; - } - -When defining `arguments`, the array definition values represent the following: - - array($name, $mode, $description, $defaultValue) - -The argument `mode` may be any of the following: `InputArgument::REQUIRED` or `InputArgument::OPTIONAL`. - - -### Defining options - -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], - ]; - } - -When defining `options`, the array definition values represent the following: - - 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 - -The `VALUE_NONE` option indicates that the option is simply used as a "switch": - - php artisan foo --option - - -### Retrieving input - -While your command is executing, you will obviously need to access the values for the arguments and options accepted by your application. To do so, you may use the `argument` and `option` methods: - -#### Retrieving the value of a command argument - - $value = $this->argument('name'); - -#### Retrieving all arguments - - $arguments = $this->argument(); - -#### Retrieving the value of a command option - - $value = $this->option('name'); - -#### Retrieving all options - - $options = $this->option(); - - -### Writing output - -To send output to the console, you may use the `info`, `comment`, `question` and `error` methods. Each of these methods will use the appropriate ANSI colors for their purpose. - -#### Sending information - - $this->info('Display this on the screen'); - -#### Sending an error message - - $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?'); - -#### Asking the user for secret input - - $password = $this->secret('What is the password?'); - -#### Asking the user for confirmation - - 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); - -#### 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(); - - $bar = $this->output->createProgressBar(count($users)); - - foreach ($users as $user) { - $this->performTask($user); - - $bar->advance(); - } - - $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). - - -## Registering commands - -#### Registering a console command - -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 - { - public function pluginDetails() - { - [...] - } - - public function register() - { - $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); - -#### 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'); - -#### 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; - }); - - $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'); - -You can also pass arguments as an array: - - $this->call('plugin:refresh', ['name' => 'Winter.Demo']); - -As well as options: - - $this->call('winter:update', ['--force' => true]); - diff --git a/console-introduction.md b/console-introduction.md new file mode 100644 index 00000000..73a6cdca --- /dev/null +++ b/console-introduction.md @@ -0,0 +1,375 @@ +# Command Line Interface + +- [Introduction](#introduction) +- [List of available commands](#command-list) +- [Building a command](#building-a-command) + - [Defining arguments](#defining-arguments) + - [Defining options](#defining-options) + - [Writing output](#writing-output) + - [Retrieving input](#retrieving-input) +- [Registering commands](#registering-commands) +- [Calling other commands](#calling-other-commands) + + +## Introduction + +Winter includes several command-line interface (CLI) commands and utilities that allow to install and manage Winter and its plugins and themes, perform site maintenance and speed up the development process. The console commands are executed through Laravel's [Artisan](http://laravel.com/docs/artisan) command-line tool. + +Commands are executed by using your terminal or shell and running the following command in the root folder of your project: + +```bash +php artisan [command] +``` + +You can get a list of available commands by not providing any command to the Artisan tool: + +```bash +php artisan +``` + +If you require help or a list of available arguments and options for each command, simply suffix the command with the `--help` flag: + +```bash +php artisan [command] --help +``` + + +## List of available commands + +The following commands are made available to every Winter installation. Click the name of a command to view more information about the usage of that command. + +Command | Description +------- | ----------- +**Setup & Maintenance** | +[`winter:install`](../console/setup-maintenance#winter-install) | Install Winter via command line. +[`winter:update`](../console/setup-maintenance#winter-update) | Update Winter and its plugins using the [Marketplace](https://wintercms.com/marketplace) via the command line. +[`winter:up`](../console/setup-maintenance#winter-up) | Run database migrations. +[`winter:passwd`](../console/setup-maintenance#winter-passwd) | Change the password of an administrator. +[`winter:env`](../console/setup-maintenance#winter-env) | Use environment files and configuration for Winter. +[`winter:version`](../console/setup-maintenance#winter-version) | Display the version of Winter in use. +[`winter:fresh`](../console/setup-maintenance#winter-fresh) | Remove the demo plugin and theme. +[`winter:mirror`](../console/setup-maintenance#winter-mirror) | Mirror publicly accessible files in another directory. +**Plugin management** | +[`plugin:install`](../console/plugin-management#plugin-install) | Download and install a plugin for Winter. +[`plugin:list`](../console/plugin-management#plugin-list) | List installed plugins. +[`plugin:rollback`](../console/plugin-management#plugin-rollback) | Rolls back a plugin and its database tables. +[`plugin:refresh`](../console/plugin-management#plugin-refresh) | Rolls back a plugin and its database tables, and re-runs all updates. +[`plugin:disable`](../console/plugin-management#plugin-disable) | Disables a plugin. +[`plugin:enable`](../console/plugin-management#plugin-enable) | Enables a plugin. +[`plugin:remove`](../console/plugin-management#plugin-install) | Removes a plugin. +**Theme management** | +[`theme:install`](../console/theme-management#theme-install) | Download and install a theme for Winter. +[`theme:list`](../console/theme-management#theme-list) | List available themes. +[`theme:use`](../console/theme-management#theme-use) | Switches Winter to the given theme. +[`theme:remove`](../console/theme-management#theme-install) | Removes a theme. +[`theme:sync`](../console/theme-management#theme-sync) | Synchronises a theme between the filesystem and the database, if you use the [Database Templates](../cms/themes#database-driven-themes) feature. +**Asset compilation (Mix)** | +[`mix:install`](../console/asset-compilation#mix-install) | Install Node dependencies for registered Mix packages. +[`mix:list`](../console/asset-compilation#mix-list) | Lists all registered Mix packages. +[`mix:compile`](../console/asset-compilation#mix-compile) | Compiles one or more Mix packages. +[`mix:watch`](../console/asset-compilation#mix-watch) | Watches changes within a Mix package and automatically compiles the package on any change. +**Scaffolding** | +[`create:theme`](../console/scaffolding#create-theme) | Create a theme. +[`create:plugin`](../console/scaffolding#create-plugin) | Create a plugin. +[`create:component`](../console/scaffolding#create-component) | Create a component in a plugin. +[`create:model`](../console/scaffolding#create-model) | Create a model in a plugin. +[`create:settings`](../console/scaffolding#create-settings) | Create a settings model in a plugin. +[`create:controller`](../console/scaffolding#create-controller) | Create a controller in a plugin. +[`create:formwidget`](../console/scaffolding#create-formwidget) | Create a form widget in a plugin. +[`create:reportwidget`](../console/scaffolding#create-reportwidget) | Create a report widget in a plugin. +[`create:command`](../console/scaffolding#create-command) | Create a console command in a plugin. +**Utilities** | +[`cache:clear`](../console/utilities#cache-clear) | Clear the application cache. +[`winter:test`](../console/utilities#winter-test) | Run unit tests on Winter and plugins. +[`winter:util`](../console/utilities#winter-util) | A collection of utilities for Winter development. + + + +## Building a command + +Plugins can also provide additional commands to augment additional functionality to Winter. + +If you wanted to create a console command called `myauthor:mycommand`, you can run the `php artisan create:command MyAuthor.MyPlugin MyCommand` [scaffolding command](../console/scaffolding#create-command) which would create the associated class for that command in a file called `plugins/myauthor/myplugin/console/MyCommand.php` with the following contents: + +```php +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 []; + } +} +``` + +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. + + +### Defining arguments + +Arguments are defined by returning an array value from the `getArguments` method are where you may define any arguments your command receives. For example: + +```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: + +```php +array($name, $mode, $description, $defaultValue) +``` + +The argument `mode` may be any of the following: `InputArgument::REQUIRED` or `InputArgument::OPTIONAL`. + + +### Defining options + +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: + +```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: + +```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: + +```bash +php artisan foo --option=bar --option=baz +``` + +The `VALUE_NONE` option indicates that the option is simply used as a "switch": + +```bash +php artisan foo --option +``` + + +### Retrieving input + +While your command is executing, you will obviously need to access the values for the arguments and options accepted by your application. To do so, you may use the `argument` and `option` methods: + +#### Retrieving the value of a command argument + +```php +$value = $this->argument('name'); +``` + +#### Retrieving all arguments + +```php +$arguments = $this->argument(); +``` + +#### Retrieving the value of a command option + +```php +$value = $this->option('name'); +``` + +#### Retrieving all options + +```php +$options = $this->option(); +``` + + +### Writing output + +To send output to the console, you may use the `info`, `comment`, `question` and `error` methods. Each of these methods will use the appropriate ANSI colors for their purpose. + +#### Sending information + +```php +$this->info('Display this on the screen'); +``` + +#### Sending an error message + +```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: + +```php +$name = $this->ask('What is your name?'); +``` + +#### Asking the user for secret input + +```php +$password = $this->secret('What is the password?'); +``` + +#### Asking the user for confirmation + +```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`: + +```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: + +```php +$users = App\User::all(); + +$bar = $this->output->createProgressBar(count($users)); + +foreach ($users as $user) { + $this->performTask($user); + + $bar->advance(); +} + +$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). + + +## Registering commands + +#### Registering a console command + +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. + +```php +class MyPlugin extends PluginBase +{ + public function pluginDetails() + { + [...] + } + + public function register() + { + $this->registerConsoleCommand('myauthor.mycommand', \MyAuthor\MyPlugin\Console\MyCommand::class); + } +} +``` + +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 could use the `Artisan::add` method to register the command: + +```php +Artisan::add(new MyAuthor\MyPlugin\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: + +```php +Artisan::resolve('myauthor.mycommand'); +``` + +#### 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: + +```php +public function boot() +{ + $this->app->singleton('myauthor.mycommand', function() { + return new \MyAuthor\MyCommand\Console\MyCommand; + }); + + $this->commands('myauthor.mycommand'); +} +``` + + +## Calling other commands + +Sometimes you may wish to call other commands from your command. You may do so using the `call` method: + +```php +$this->call('winter:up'); +``` + +You can also pass arguments as an array: + +```php +$this->call('plugin:refresh', ['name' => 'Winter.Demo']); +``` + +As well as options: + +```php +$this->call('winter:update', ['--force' => true]); +``` diff --git a/console-plugin-management.md b/console-plugin-management.md new file mode 100644 index 00000000..f7e3d708 --- /dev/null +++ b/console-plugin-management.md @@ -0,0 +1,84 @@ +# Plugin Management Commands + +- [Download and install a plugin for Winter](#plugin-install) +- [List installed plugins](#plugin-list) +- [Refresh a plugin](#plugin-refresh) +- [Rollback a plugin](#plugin-rollback) +- [Enable a plugin](#plugin-enable) +- [Disable a plugin](#plugin-disable) +- [Remove a plugin](#plugin-remove) + +The following commands are used for managing plugins within your Winter installation. + + +## Download and install a plugin for Winter + +```bash +php artisan plugin:install +``` + +The `plugin:install` command downloads and installs the plugin by its plugin code in the format **AuthorName.PluginName**. You can retrieve the plugin code through the Winter marketplace. + +Note that your installation should be bound to a project in order to use this command. You can create projects on Winter website, in the [Account / Projects](https://wintercms.com/account/project/dashboard) section. + +> **NOTE:** If you have already have the plugin files locally either through Composer or manually uploading them then you can just run [`winter:up`](#console-up-command) to run the plugin's pending migrations to "install" it. This command is mostly meant for instaling plugins sourced from the [Winter CMS Marketplace](https://wintercms.com/marketplace). + + +## List installed plugins + +```bash +php artisan plugin:list +``` + +The `plugin:list` command will generate a table of installed plugins in your Winter installation, including the version installed, whether the plugin is enabled or disabled and if updates are frozen for the plugin or not. + +Each plugin is listed by its plugin code, allowing you to use the code for other plugin commands listed here. + + +## Refresh a plugin + +```bash +php artisan plugin:refresh +``` + +The `plugin:refresh` command allows you to rollback a plugin, destroying its database records and tables, and re-run all updates on the plugin. **This is a destructive action.** You will be prompted to confirm the action before proceeding. + +This command is made available mainly for plugin development. + + +## Rollback a plugin + +```bash +php artisan plugin:rollback [version] +``` + +The `plugin:rollback` command allows you to rollback a plugin, optionally to a specified version. It can be useful for rolling back a plugin which has introduced an error in your Winter installation, rolling it back to a version that worked previously. **This is a destructive action.** You will be prompted to confirm the action before proceeding. + +The `version` argument is optional - if it is not specified, the plugin is rolled back completely. + + +## Enable a plugin + +```bash +php artisan plugin:enable +``` + +The `plugin:enable` command allows you to enable a previously disabled plugin. The plugin will be able to function in your Winter installation once more. + + +## Disable a plugin + +```bash +php artisan plugin:disable +``` + +The `plugin:disable` command allows you to disable a previously enabled plugin. The plugin will no longer function in your Winter installation. If the plugin disabled is a requirement of another plugin installed, that plugin will also be disabled. + + +## Remove a plugin + +```bash +php artisan plugin:remove +``` + +The `plugin:remove` command allows you remove a plugin installed on your Winter CMS installation. This will remove both the files for the plugin, and the database records and tables. **This is a destructive action.** You will be prompted to confirm the action before proceeding. \ No newline at end of file diff --git a/console-scaffolding.md b/console-scaffolding.md index bc269129..d6a32801 100644 --- a/console-scaffolding.md +++ b/console-scaffolding.md @@ -1,80 +1,94 @@ # Scaffolding Commands -- [Scaffolding commands](#scaffolding-commands) - - [Create a theme](#scaffold-create-theme) - - [Create a plugin](#scaffold-create-plugin) - - [Create a component](#scaffold-create-component) - - [Create a model](#scaffold-create-model) - - [Create a settings model](#scaffold-create-settings-model) - - [Create a backend controller](#scaffold-create-controller) - - [Create a form widget](#scaffold-create-formwidget) - - [Create a report widget](#scaffold-create-reportwidget) - - [Create a console command](#scaffold-create-command) +- [Create a theme](#create-theme) +- [Create a plugin](#create-plugin) +- [Create a component](#create-component) +- [Create a model](#create-model) +- [Create a settings model](#create-settings-model) +- [Create a backend controller](#create-controller) +- [Create a form widget](#create-formwidget) +- [Create a report widget](#create-reportwidget) +- [Create a console command](#create-command) - -## Scaffolding commands +The following commands allow you to quickly scaffold additional code into your Winter project, speeding up development time. -Use the scaffolding commands to speed up the development process. + +## Create a theme - -### Create a Theme +```bash +php artisan create:theme +``` -The `create:theme` command generates a theme folder and basic files for the theme. The parameter specifies the theme code. +The `create:theme` command generates a theme folder and basic files for the theme. The first argument specifies the theme code, eg. `myauthor-mytheme`. - php artisan create:theme myauthor-mytheme + +## Create a plugin - -### Create a Plugin +```bash +php artisan create:plugin +``` -The `create:plugin` command generates a plugin folder and basic files for the plugin. The parameter specifies the author and plugin name. +The `create:plugin` command generates a plugin folder and basic files for the plugin. The first argument specifies the author and plugin name, eg. `MyAuthor.MyPlugin`. - php artisan create:plugin Acme.Blog + +## Create a component - -### Create a Component +```bash +php artisan create: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. +The `create:component` command creates a new component class and the default component view. The first argument specifies the plugin code of the plugin that this component will be added into, and the second parameter specifies the component class name, eg. `MyComponent`. - php artisan create:component Acme.Blog Post + +## Create a model - -### Create a Model +```bash +php artisan create: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. +The `create:model` command generates the files needed for a new model. The first argument specifies the plugin code of the plugin that this model will be added into, and the second parameter specifies the model class name, eg. `MyModel`. - php artisan create:model Acme.Blog Post + +## Create a settings model - -### Create a Settings Model +```bash +php artisan create:settings [model name] +``` -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`). +The `create:settings` command generates the files needed for a new [Settings model](../plugin/settings#database-settings). The first argument specifies the plugin code of the plugin that this model will be added into, and the second parameter is optional and specifies the Settings model class name (defaults to `Settings`). - php artisan create:settings Acme.Blog CustomSettings + +## Create a backend controller - -### Create a backend Controller +```bash +php artisan create: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. +The `create:controller` command generates a controller, configuration and view files. The first argument specifies the plugin code of the plugin that this controller will be added into, and the second parameter specifies the controller class name, eg. `MyController`. - php artisan create:controller Acme.Blog Posts + +## Create a form widget - -### Create a FormWidget +```bash +php artisan create: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. +The `create:formwidget` command generates a backend form widget, view and basic asset files. The first argument specifies the plugin code of the plugin that this form widget will be added into, and the second parameter specifies the form widget class name, eg. `MyFormWidget`. - php artisan create:formwidget Acme.Blog CategorySelector + +## Create a report widget - -### Create a ReportWidget +```bash +php artisan create: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. +The `create:reportwidget` command generates a backend report widget, view and basic asset files. The first argument specifies the plugin code of the plugin that this report widget will be added into, and the second parameter specifies the report widget class name, eg. `MyReportWidget`. - php artisan create:reportwidget Acme.Blog TopPosts + +## Create a console command - -### Create a console Command +```bash +php artisan create: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 +The `create:command` command generates a [new console command](../console/development). The first argument specifies the plugin code of the plugin that this console command will be added into, and the second parameter specifies the command name. diff --git a/console-setup-maintenance.md b/console-setup-maintenance.md new file mode 100644 index 00000000..50808e50 --- /dev/null +++ b/console-setup-maintenance.md @@ -0,0 +1,107 @@ +# Setup & Maintenance Commands + +- [Install Winter via command line](#winter-install) +- [Update Winter and its plugins](#winter-update) +- [Run database migrations](#winter-up) +- [Change an administrator's password](#winter-passwd) +- [Configure Winter through an environment file](#winter-env) +- [Get the installed Winter version](#winter-version) +- [Remove the demo plugin and theme](#winter-fresh) +- [Mirror public files](#winter-mirror) + +The following commands are used for the setup and maintenance of a Winter installation. + + +## Install Winter via command line + +```bash +php artisan winter:install +``` + +The `winter:install` command will guide you through the process of setting up Winter CMS for the first time. It will ask for the database configuration, application URL, encryption key and administrator details. + +You also may wish to inspect **config/app.php** and **config/cms.php** to change any additional configuration. + +> **NOTE:** You cannot run `winter:install` after running [`winter:env`](#winter-env). The `winter:env` command takes the existing configuration values and puts them in the `.env` file while replacing the original values with calls to `env()` within the configuration files. `winter:install` cannot replace those calls to `env()` within the configuration files as that would be overly complex to manage. + + +## Update Winter and its plugins + +```bash +php artisan winter:update +``` + +The `winter:update` command will request updates from the Winter gateway. It will update the core application and plugin files, then perform a database migration. + +> **IMPORTANT**: If you are using [using Composer](../help/using-composer), do **NOT** run this command without first making sure that `cms.disableCoreUpdates` is set to `true`. Doing so will cause conflicts between the marketplace version of Winter and the version available through Composer. In order to update the core Winter installation when using Composer, run `composer update` instead. + + +## Run database migrations + +```bash +php artisan winter:up +php artisan winter:down +``` + +The `winter:up` command will perform a database migration, creating database tables and executing seed scripts, provided by the system and [plugin version history](../plugin/updates). The migration command can be run multiple times - it will only execute a migration or seed script once, which means only new changes are applied. + +The inverse command `winter:down` will reverse all migrations, dropping database tables and deleting data. Care should be taken when using this command. The [plugin refresh command](#plugin-refresh-command) is a useful alternative for debugging a single plugin. + + +## Change an administrator's password + +```bash +php artisan winter:passwd [username] [password] +``` + +The `winter:passwd` command will allow the password of a backend user or administrator to be changed via the command-line. This is useful if someone gets locked out of their Winter CMS install, or for changing the password for the default administrator account. + +You may provide the username/email and password as both the first and second argument, or you may leave the arguments blank, in which case the command will be run interactively. + + +## Configure Winter through an environment file + +```bash +php artisan winter:env +``` + +The `winter:env` command allows you to convert the configuration of Winter to use an environment variable file. The command will create a `.env` file in the root folder of your project, and change certain configuration variables in the `config` folder to use these environment variables instead. + +This setup is recommend if you use automated deployment tools, and provides a level of security by removing passwords or sensitive information from your configuration files (which are normally stored in source control) and places them in the environment file, which you should not include in source control. + +You are not restricted from providing environment variables through another method, for example, you may store the environment variables in your server's environment, or provide them through the PHP configuration. + + +## Get the installed Winter version + +```bash +php artisan winter:version [--changes] +``` + +The `winter:version` command displays the installed version of Winter. This is determined by querying a [central build manifest](https://github.com/wintercms/meta/blob/master/manifest/builds.json) and verifying the integrity of each system file in Winter against each build in this manifest. This allows the command to determine if any modifications have been made to the system files. + +If modifications are detected, this command will try and best-guess which version is installed, but will alert you that modifications have been made. + +If you wish to review the files that have been modified, you can add the `--changes` flag to be provided with a list of files that have been added, modified or removed from Winter. + + +## Remove the demo plugin and theme + +```bash +php artisan winter:fresh +``` + +The `winter:fresh` command will remove the demo plugin and theme that is included with every Winter installation, if these are still found in your installation. + + +## Mirror public files + +```bash +php artisan winter:mirror public [--relative] +``` + +The `winter:mirror` command creates a mirrored copy of the public files needed to serve the application, using symbolic linking. This command is used when [setting up a public folder](../setup/configuration#public-folder) and is recommended for security purposes as it prevents direct access to system files. + +This command should be re-run whenever plugins and themes are installed or removed. + +By default, this command will create absolute symlink paths. If you wish to use relative paths instead, you may add the `--relative` flag to do so. \ No newline at end of file diff --git a/console-theme-management.md b/console-theme-management.md new file mode 100644 index 00000000..815f3a10 --- /dev/null +++ b/console-theme-management.md @@ -0,0 +1,80 @@ +# Theme Management Commands + +- [Download and install a theme for Winter](#theme-install) +- [List installed themes](#theme-list) +- [Switch theme](#theme-use) +- [Remove a theme](#theme-remove) +- [Synchronise database templates](#theme-sync) + +The following commands are used for managing themes within your Winter installation. + + +## Download and install a theme for Winter + +```bash +php artisan theme:install [directory] +``` + +The `theme:install` command downloads and installs the theme by its theme code in the format **AuthorName.ThemeName**. You can retrieve the theme code through the Winter marketplace. + +By default, the theme will be installed in the `themes` folder, in a subdirectory `authorname-themename`. You can customise the subdirectory name by specifying the optional `directory` argument. + + +## List installed themes + +```bash +php artisan theme:list +``` + +The `theme:list` command will present a list of themes installed in the Winter installation. It will also display whether the theme is active or not besides each theme item. + + +## Switch theme + +```bash +php artisan theme:use +``` + +The `theme:use` command allows you to switch to a specific theme for your Winter installation. This theme will then be used for the public pages on your project. + + +## Remove a theme + +```bash +php artisan theme:remove +``` + +The `theme:remove` command allows you remove a theme installed on your Winter CMS installation. This will remove the files for theme. **This is a destructive action.** You will prompted to confirm the action before proceeding. + + +## Synchronise database templates + +```bash +php artisan theme:sync [theme code] [--target=] [--force] [--paths=] +``` + +The `theme:sync` command synchronises a theme's content between the filesystem and database when the [Database Templates](../cms/themes#database-driven-themes) feature is enabled. + +By default the theme that will be synchronised is the currently active theme. You can specify any theme to sync by passing the desired theme's code as the `theme code` argument. + +```bash +php artisan theme:sync my-custom-theme +``` + +By default, the sync direction will be from the database to the filesytem (i.e. you're syncing changes on a remote host to the filesystem for tracking in a version control system). However, you can change the direction of the sync by specifying `--target=database`. This is useful if you have changed the underlying files that make up the theme and you want to force the site to pick up your changes even if they have made changes of their own that are stored in the database. + +```bash +php artisan theme:sync --target=database +``` + +By default the command requires user interaction to confirm that they want to complete the sync (including information about the amount of paths affected, the theme targeted, and the target & source of the sync). To override the need for user interaction (i.e. if running this command in a deploy / build script of some sort) just pass the `--force` option: + +```bash +php artisan theme:sync --force +``` + +Unless otherwise specified, the command will sync all the valid paths (determined by the Halcyon model instances returned to the `system.console.theme.sync.getAvailableModelClasses` event) available in the theme. To manually specify specific paths to be synced pass a comma separated list of paths to the `--paths` option: + +```bash +php artisan theme:sync --paths=partials/header.htm,content/contact.md +``` \ No newline at end of file diff --git a/console-utilities.md b/console-utilities.md new file mode 100644 index 00000000..bd299a67 --- /dev/null +++ b/console-utilities.md @@ -0,0 +1,125 @@ +# Utility Commands + +- [Clear the cache](#clear-cache) +- [Run unit tests](#winter-test) +- [Utility runner](#winter-util) + - [Compile Winter assets](#winter-util-compile-assets) + - [Git pull](#winter-util-git-pull) + - [Purge thumbnails](#winter-util-purge-thumbs) + - [Purge uploads](#winter-util-purge-uploads) + - [Purge orphaned uploads](#winter-util-purge-orphans) + +The following commands are utility commands available on Winter installations. + + +## Clear the cache + +```bash +php artisan cache:clear +``` + +The `cache:clear` command flushes the entire application cache that is used to increase the performance of Winter. We routinely cache compiled template files and system data, however, in rare cases this cached data can result in your project showing outdated data or not reflecting more recent changes. This command will delete the cache and ensure the latest data is available. + + +## Run unit tests + +```bash +php artisan winter:test [--core] [--plugin=] [--configuration=] +``` + +The `winter:test` command runs the unit tests for the entire project, a specific plugin, or the Winter core. + +To run the entire project's unit tests: + +```bash +php artisan winter:test +``` + +Or, to run only the core unit tests, use the `-o` or `--core` option: + +```bash +php artisan winter:test -o +``` + +To run a specific plugin's tests, use the `-p` or `--plugin=` option: + +```bash +php artisan winter:test -p Acme.Demo +``` + +To run a custom test suite, use the `-c` or `--configuration=` option: + +```bash +php artisan winter:test -c ./custom-path/phpunit.xml +``` + +If using additional PHPUnit parameters / options, they must be included after the winter:test command's options: + +```bash +php artisan winter:test -p Acme.Demo --filter=FilteredTest --stop-on-failure +``` + + +## Utility runner + +`winter:util` - a generic command to perform general utility tasks, such as cleaning up files or combining files. The arguments passed to this command will determine the task used. + + +### Compile Winter assets + +Outputs combined system files for JavaScript (js), StyleSheets (less), client side language (lang), or everything (assets). + +```bash +php artisan winter:util compile assets +php artisan winter:util compile lang +php artisan winter:util compile js +php artisan winter:util compile less +``` + +To combine without minification, pass the `--debug` option. + +```bash +php artisan winter:util compile js --debug +``` + + +### Pull all repos + +This will execute the command `git pull` on all theme and plugin directories. + +```bash +php artisan winter:util git pull +``` + + +### Purge thumbnails + +Deletes all generated thumbnails in the uploads directory. + +```bash +php artisan winter:util purge thumbs +``` + + +### Purge uploads + +Deletes files in the uploads directory that do not exist in the "system_files" table. + +```bash +php artisan winter:util purge uploads +``` + + +### Purge orphaned uploads + +Deletes records in "system_files" table that do not belong to any other model. + +```bash +php artisan winter:util purge orphans +``` + +To also delete records that have no associated file in the local storage, pass the `--missing-files` option. + +```bash +php artisan winter:util purge orphans --missing-files +``` diff --git a/database-attachments.md b/database-attachments.md index 036e8d5a..5279dce4 100644 --- a/database-attachments.md +++ b/database-attachments.md @@ -1,11 +1,19 @@ # Database: File Attachments +- [Introduction](#introduction) - [File attachments](#file-attachments) - [Creating new attachments](#creating-attachments) - [Viewing attachments](#viewing-attachments) - [Usage example](#attachments-usage-example) - [Validation example](#attachments-validation-example) + +## Introduction + +Winter CMS provides a few different ways to manage files depending on your needs. File attachments are files that are stored on the filesystem and have database records associated with them in order to simplify connecting them to other database records. + +When using the `System\Models\File` model, you are able to configure the disk and paths that are used to store and retrieve the files managed by that model by modifying the `storage.uploads` setting in the [`config/cms.php` file](https://github.com/wintercms/winter/blob/develop/config/cms.php#L317). + ## File attachments @@ -15,84 +23,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`: +Protected attachments are uploaded to the File Upload disk'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 +138,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 +224,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 8baee589..7ce1013e 100644 --- a/database-model.md +++ b/database-model.md @@ -25,15 +25,17 @@ Winter provides a beautiful and simple Active Record implementation for working Model classes reside in the **models** subdirectory of a plugin directory. An example of a model directory structure: - plugins/ - acme/ - blog/ - models/ - user/ <=== Model config directory - columns.yaml <=== Model config files - fields.yaml <==^ - User.php <=== Model class - Plugin.php +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ blog + ┣ πŸ“‚ models + ┃ ┣ πŸ“‚ user <=== Model config directory + ┃ ┃ ┣ πŸ“œ columns.yaml <=== Model config files + ┃ ┃ β”— πŸ“œ fields.yaml <==^ + ┃ β”— πŸ“œ User.php <=== Model class + β”— πŸ“œ Plugin.php +``` The model configuration directory could contain the model's [list column](../backend/lists#list-columns) and [form field](../backend/forms#form-fields) definitions. The model configuration directory name matches the model class name written in lowercase. @@ -42,19 +44,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,104 +67,122 @@ 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 ------------- | ------------- -**$primaryKey** | primary key name used to identify the model. -**$incrementing** | boolean that if false indicates that the primary key is not an incrementing integer value. -**$exists** | boolean that if true indicates that the model exists. -**$dates** | values are converted to an instance of Carbon/DateTime objects after fetching. -**$timestamps** | boolean that if true will automatically set created_at and updated_at fields. -**$jsonable** | values are encoded as JSON before saving and converted to arrays after fetching. -**$fillable** | values are fields accessible to [mass assignment](#mass-assignment). -**$guarded** | values are fields guarded from [mass assignment](#mass-assignment). -**$visible** | values are fields made visible when [serializing the model data](../database/serialization). -**$hidden** | values are fields made hidden when [serializing the model data](../database/serialization). -**$connection** | string that contains the [connection name](../database/basics#accessing-connections) that's utilised by the model by default. +`$primaryKey` | primary key name used to identify the model. +`$incrementing` | boolean that if false indicates that the primary key is not an incrementing integer value. +`$exists` | boolean that if true indicates that the model exists. +`$dates` | values are converted to an instance of Carbon/DateTime objects after fetching. +`$timestamps` | boolean that if true will automatically set created_at and updated_at fields. +`$jsonable` | values are encoded as JSON before saving and converted to arrays after fetching. +`$fillable` | values are fields accessible to [mass assignment](#mass-assignment). +`$guarded` | values are fields guarded from [mass assignment](#mass-assignment). +`$visible` | values are fields made visible when [serializing the model data](../database/serialization). +`$hidden` | values are fields made hidden when [serializing the model data](../database/serialization). +`$connection` | string that contains the [connection name](../database/basics#accessing-connections) that's utilised by the model by default. #### Primary key 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 +196,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 +230,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 +256,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 +304,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 +317,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 +337,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 +355,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 +393,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 +454,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 +517,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 + .attributes-table-precessor + table td:first-child, + .attributes-table-precessor + table td:first-child > * { white-space: nowrap; } + +
+ Event | Description ------------- | ------------- -**beforeCreate** | before the model is saved, when first created. -**afterCreate** | after the model is saved, when first created. -**beforeSave** | before the model is saved, either created or updated. -**afterSave** | after the model is saved, either created or updated. -**beforeValidate** | before the supplied model data is validated. -**afterValidate** | after the supplied model data has been validated. -**beforeUpdate** | before an existing model is saved. -**afterUpdate** | after an existing model is saved. -**beforeDelete** | before an existing model is deleted. -**afterDelete** | after an existing model is deleted. -**beforeRestore** | before a soft-deleted model is restored. -**afterRestore** | after a soft-deleted model has been restored. -**beforeFetch** | before an existing model is populated. -**afterFetch** | after an existing model has been populated. +`beforeCreate` | before the model is saved, when first created. +`afterCreate` | after the model is saved, when first created. +`beforeSave` | before the model is saved, either created or updated. +`afterSave` | after the model is saved, either created or updated. +`beforeValidate` | before the supplied model data is validated. +`afterValidate` | after the supplied model data has been validated. +`beforeUpdate` | before an existing model is saved. +`afterUpdate` | after an existing model is saved. +`beforeDelete` | before an existing model is deleted. +`afterDelete` | after an existing model is deleted. +`beforeRestore` | before a soft-deleted model is restored. +`afterRestore` | after a soft-deleted model has been restored. +`beforeFetch` | before an existing model is populated. +`afterFetch` | after an existing model has been populated. An example of using an event: - /** - * 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); +} +``` > **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 +680,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 +728,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..a832551e 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 50248190..bd62bbbf 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,58 +68,72 @@ 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: + +
+ Argument | Description ------------- | ------------- -**order** | sorting order for multiple records. -**conditions** | filters the relation using a raw where query statement. -**scope** | filters the relation using a supplied scope method. -**push** | if set to false, this relation will not be saved via `push`, default: true. -**delete** | if set to true, the related model will be deleted if the primary model is deleted or relationship is destroyed, default: false. -**detach** | if set to false the related model will not be automatically detached if the primary model is deleted or the relationship is destroyed. Used by `belongsToMany` relationships only, default: true. -**count** | if set to true, the result contains a `count` column only, used for counting relations, default: false. +`order` | sorting order for multiple records. +`conditions` | filters the relation using a raw where query statement. +`scope` | filters the relation using a supplied scope method. +`push` | if set to false, this relation will not be saved via `push`, default: true. +`delete` | if set to true, the related model will be deleted if the primary model is deleted or relationship is destroyed, default: false. +`detach` | if set to false the related model will not be automatically detached if the primary model is deleted or the relationship is destroyed. Used by `belongsToMany` relationships only, default: true. +`count` | if set to true, the result contains a `count` column only, used for counting relations, default: false. 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 - { - public $belongsToMany = [ - 'categories' => [ - 'Acme\Blog\Models\Category', - 'scope' => 'isActive' - ] - ]; - } - - class Category extends Model +class Category extends Model +{ + public function scopeIsActive($query) { - 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 +152,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 +321,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,41 +397,53 @@ 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: + +
+ Argument | Description ------------- | ------------- -**table** | the name of the join table. -**key** | the key column name of the defining model (inside pivot table). Default value is combined from model name and `_id` suffix, i.e. `user_id` -**parentKey** | the key column name of the defining model (inside defining model table). Default: id -**otherKey** | the key column name of the related model (inside pivot table). Default value is combined from model name and `_id` suffix, i.e. `role_id` -**relatedKey** | the key column name of the related model (inside related model table). Default: id -**pivot** | an array of pivot columns found in the join table, attributes are available via `$model->pivot`. -**pivotModel** | specify a custom model class to return when accessing the pivot relation. Defaults to `Winter\Storm\Database\Pivot`. -**timestamps** | if true, the join table should contain `created_at` and `updated_at` columns. Default: false +`table` | the name of the join table. +`key` | the key column name of the defining model (inside pivot table). Default value is combined from model name and `_id` suffix, i.e. `user_id` +`parentKey` | the key column name of the defining model (inside defining model table). Default: id +`otherKey` | the key column name of the related model (inside pivot table). Default value is combined from model name and `_id` suffix, i.e. `role_id` +`relatedKey` | the key column name of the related model (inside related model table). Default: id +`pivot` | an array of pivot columns found in the join table, attributes are available via `$model->pivot`. +`pivotModel` | specify a custom model class to return when accessing the pivot relation. Defaults to `Winter\Storm\Database\Pivot`. Note: `pivot` still needs to be defined in order to include the pivot columns in any database queries. +`timestamps` | if true, the join table should contain `created_at` and `updated_at` columns. Default: false ### Has Many Through @@ -388,29 +468,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 +514,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 +574,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 +644,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 +727,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 +779,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 +797,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 +839,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 +```sql +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 +987,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 +1022,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 +1063,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 +1112,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 +1202,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 +1227,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 +1293,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..bc4c5770 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']; +} +``` 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. diff --git a/help-unit-testing.md b/help-unit-testing.md index ddf01124..f770c353 100644 --- a/help-unit-testing.md +++ b/help-unit-testing.md @@ -6,88 +6,96 @@ ## Testing plugins -Individual plugin test cases can be run by running [the `winter:test` command](../console/commands/#winter-test-command) with the `--p|plugin=` option. +Individual plugin test cases can be run by running [the `winter:test` command](../console/utilities#winter-test) with the `--p|plugin=` option. ### Creating plugin tests Plugins can be tested by creating a file called `phpunit.xml` in the base directory with the following content, for example, in a file **/plugins/acme/blog/phpunit.xml**: - - - - - ./tests - - - - - - - - +```xml + + + + + ./tests + + + + + + + + +``` Then a **tests/** directory can be created to contain the test classes. The file structure should mimic the base directory with classes having a `Test` suffix. Using a namespace for the class is also recommended. - 'Hi!']); - $this->assertEquals(1, $post->id); - } + $post = Post::create(['title' => 'Hi!']); + $this->assertEquals(1, $post->id); } +} +``` The test class should extend the base class `PluginTestCase` and this is a special class that will set up the Winter database stored in memory, as part of the `setUp` method. It will also refresh the plugin being tested, along with any of the defined dependencies in the plugin registration file. This is the equivalent of running the following before each test: - php artisan winter:up - php artisan plugin:refresh Acme.Blog - [php artisan plugin:refresh , ...] +```bash +php artisan winter:up +php artisan plugin:refresh Acme.Blog +[php artisan plugin:refresh , ...] +``` > **NOTE:** If your plugin uses [configuration files](../plugin/settings#file-configuration), then you will need to run `System\Classes\PluginManager::instance()->registerAll(true);` in the `setUp` method of your tests. Below is an example of a base test case class that should be used if you need to test your plugin working with other plugins instead of in isolation. - use System\Classes\PluginManager; +```php +use System\Classes\PluginManager; - class BaseTestCase extends PluginTestCase +class BaseTestCase extends PluginTestCase +{ + public function setUp(): void { - public function setUp(): void - { - parent::setUp(); + parent::setUp(); - // Get the plugin manager - $pluginManager = PluginManager::instance(); + // Get the plugin manager + $pluginManager = PluginManager::instance(); - // Register the plugins to make features like file configuration available - $pluginManager->registerAll(true); + // Register the plugins to make features like file configuration available + $pluginManager->registerAll(true); - // Boot all the plugins to test with dependencies of this plugin - $pluginManager->bootAll(true); - } + // Boot all the plugins to test with dependencies of this plugin + $pluginManager->bootAll(true); + } - public function tearDown(): void - { - parent::tearDown(); + public function tearDown(): void + { + parent::tearDown(); - // Get the plugin manager - $pluginManager = PluginManager::instance(); + // Get the plugin manager + $pluginManager = PluginManager::instance(); - // Ensure that plugins are registered again for the next test - $pluginManager->unregisterAll(); - } + // Ensure that plugins are registered again for the next test + $pluginManager->unregisterAll(); } +} +``` #### Changing database engine for plugins tests @@ -102,7 +110,7 @@ To perform unit testing on the core Winter files, you should download a developm ### Unit tests -Unit tests can be performed by running [the `winter:test` command](../console/commands/#winter-test-command) with the `--o|-core` option. +Unit tests can be performed by running [the `winter:test` command](../console/utilities#winter-test) with the `--o|-core` option. ### Functional tests diff --git a/help-using-composer.md b/help-using-composer.md index ba4ae83d..cbb82553 100644 --- a/help-using-composer.md +++ b/help-using-composer.md @@ -4,8 +4,9 @@ - [Converting from a basic installation](#converting-from-basic-install) - [Development branch](#development-branch) - [Deployment best practices](#deployment-best-practices) +- [Installing Winter via Composer](#installing-winter) - [Installing a plugin or theme](#installing-plugin-theme) -- [Publishing blugins or themes](#publishing-products) +- [Publishing plugins or themes](#publishing-products) - [Package descriptions](#package-descriptions) - [Marketplace builds](#marketplace-builds) - [Using Laravel packages](#laravel-packages) @@ -16,7 +17,7 @@ ## Introduction -Using [Composer](https://getcomposer.org/) as an alternative package manager to using the standard one-click update manager is recommended for more advanced users and developers. See the console command on how to [first install Winter using composer](../console/commands#console-install-composer). +Using [Composer](https://getcomposer.org/) as an alternative package manager to using the standard one-click update manager is recommended for more advanced users and developers. Composer is the de-facto standard for package management in the PHP ecosystem, and can handle the downloading, installation and management of Winter CMS plugins and themes, as well as third-party Laravel packages and vendor libraries. @@ -32,11 +33,11 @@ In order to use Composer with a Winter CMS instance that has been installed usin If you plan on submitting pull requests to the Winter CMS project via GitHub, or are actively developing a project based on Winter CMS and want to stay up to date with the absolute latest version, we recommend switching your composer dependencies to point to the `develop` branch where all the latest improvements and bug fixes take place. Doing this will allow you to catch any potential issues that may be introduced (as rare as they are) right when they happen and get them fixed while you're still actively working on your project instead of only discovering them several months down the road if they eventually make it into production. -``` -"winter/storm": "dev-develop as 1.1", -"winter/wn-system-module": "dev-develop", -"winter/wn-backend-module": "dev-develop", -"winter/wn-cms-module": "dev-develop", +```json +"winter/storm": "dev-develop as 1.1.999", +"winter/wn-system-module": "dev-develop as 1.1.999", +"winter/wn-backend-module": "dev-develop as 1.1.999", +"winter/wn-cms-module": "dev-develop as 1.1.999", "laravel/framework": "~6.0", ``` @@ -52,6 +53,27 @@ Using the following best practices with Composer and Winter CMS will make deploy - Add a `.gitignore` file inside the `plugins` folder to ignore all changes within this folder if you install your plugins via Composer. You can optionally allow custom plugins that are only being used for that specific project. - Use `composer install --no-dev` on your production instance to specifically exclude any "development" packages and libraries that won't be used in production. + +## Installing Winter via Composer + +Installing Winter via Composer is easy. You can use the `create-project` command through Composer to quickly set up a new Winter installation. + +```bash +composer create-project wintercms/winter + +# Example: +# composer create-project wintercms/winter mywinter +``` + +If you wish to install a specific version of Winter, you can also specify the version. + +```bash +composer create-project wintercms/winter "" + +# Example: +# composer create-project wintercms/winter mywinter "1.0.474" +``` + ## Installing a plugin or theme using Composer @@ -89,24 +111,26 @@ composer require --dev "" When publishing your plugins or themes to the marketplace, you may wish to also make them available via Composer. An example `composer.json` file for a plugin is included below: - { - "name": "winter/wn-demo-plugin", - "type": "winter-plugin", - "description": "Demo Winter CMS plugin", - "keywords": ["winter", "cms", "demo", "plugin"], - "license": "MIT", - "authors": [ - { - "name": "Winter CMS Maintainers", - "url": "https://wintercms.com", - "role": "Maintainer" - } - ], - "require": { - "php": ">=7.2", - "composer/installers": "~1.0" +```json +{ + "name": "winter/wn-demo-plugin", + "type": "winter-plugin", + "description": "Demo Winter CMS plugin", + "keywords": ["winter", "cms", "demo", "plugin"], + "license": "MIT", + "authors": [ + { + "name": "Winter CMS Maintainers", + "url": "https://wintercms.com", + "role": "Maintainer" } + ], + "require": { + "php": ">=7.2", + "composer/installers": "~1.0" } +} +``` Be sure to start your package `name` with **wn-** and end it with **-plugin** or **-theme** respectively - this will help others find your package and is in accordance with the [quality guidelines](../help/developer/guide#repository-naming). @@ -166,47 +190,51 @@ However, this can create problems with Winter's plugin oriented design, since th You may place this code in your Plugin registration file and call it from the the `boot()` method. - public function bootPackages() - { - // Get the namespace code of the current plugin - $pluginNamespace = str_replace('\\', '.', strtolower(__NAMESPACE__)); - - // Locate the packages to boot - $packages = \Config::get($pluginNamespace . '::packages'); - - // Boot each package - foreach ($packages as $name => $options) { - // Apply the configuration for the package - if ( - !empty($options['config']) && - !empty($options['config_namespace']) - ) { - Config::set($options['config_namespace'], $options['config']); - } +```php +public function bootPackages() +{ + // Get the namespace code of the current plugin + $pluginNamespace = str_replace('\\', '.', strtolower(__NAMESPACE__)); + + // Locate the packages to boot + $packages = \Config::get($pluginNamespace . '::packages'); + + // Boot each package + foreach ($packages as $name => $options) { + // Apply the configuration for the package + if ( + !empty($options['config']) && + !empty($options['config_namespace']) + ) { + Config::set($options['config_namespace'], $options['config']); } } +} +``` Now you are free to provide the packages configuration values the same way you would with regular plugin configuration values. - return [ - // Laravel Package Configuration - 'packages' => [ - 'packagevendor/packagename' => [ - // The accessor for the config item, for example, - // to access via Config::get('purifier.' . $key) - 'config_namespace' => 'purifier', - - // The configuration file for the package itself. - // Copy this from the package configuration. - 'config' => [ - 'encoding' => 'UTF-8', - 'finalize' => true, - 'cachePath' => storage_path('app/purifier'), - 'cacheFileMode' => 0755, - ], +```php +return [ + // Laravel Package Configuration + 'packages' => [ + 'packagevendor/packagename' => [ + // The accessor for the config item, for example, + // to access via Config::get('purifier.' . $key) + 'config_namespace' => 'purifier', + + // The configuration file for the package itself. + // Copy this from the package configuration. + 'config' => [ + 'encoding' => 'UTF-8', + 'finalize' => true, + 'cachePath' => storage_path('app/purifier'), + 'cacheFileMode' => 0755, ], ], - ]; + ], +]; +``` Now the package configuration has been included natively in Winter CMS and the values can be changed normally using the [standard configuration approach](../plugin/settings#file-configuration). diff --git a/images/header-snowboard.png b/images/header-snowboard.png new file mode 100644 index 00000000..d864bef8 Binary files /dev/null and b/images/header-snowboard.png differ diff --git a/markup-filter-app.md b/markup-filter-app.md index e8b6a695..2e3471b0 100644 --- a/markup-filter-app.md +++ b/markup-filter-app.md @@ -2,22 +2,34 @@ The `| app` filter returns an address relative to the public path of the website. The result is an absolute URL, including the domain name and protocol, to the location specified in the filter parameter. The filter can be applied to any path. - +>**NOTE**: If an absolute URL is passed to this filter it will be returned unmodified. Only relative URLs are turned into absolute URLs relative to the web root. + +```twig + +``` If the website address is __https://example.com__ the above example would output the following: - +```html + +``` + +>**NOTE**: When linking to static assets it is recommended to use the [`| asset`](filter-asset) filter instead. It can also be used for static URLs: - - About Us - +```twig + + About Us + +``` The above would output: - - About us - +```html + + About us + +``` > **NOTE**: The `| page` filter is recommended for linking to other pages. diff --git a/markup-filter-asset.md b/markup-filter-asset.md new file mode 100644 index 00000000..c9ef61db --- /dev/null +++ b/markup-filter-asset.md @@ -0,0 +1,13 @@ +# | asset + +The asset filter (also available as a function, `asset()`) generates a URL for an asset using the current scheme of the request (HTTP or HTTPS): + +```twig +{{ 'img/photo.jpg' | asset }} + +{# or #} + +{{ asset('img/photo.jpg') }} +``` + +See [Helpers#asset](../services/helpers#method-asset) for more information. diff --git a/markup-filter-default.md b/markup-filter-default.md index 3a0ee582..47acacdd 100644 --- a/markup-filter-default.md +++ b/markup-filter-default.md @@ -2,14 +2,18 @@ The `| default` filter returns the value passed as the first argument if the filtered value is undefined or empty, otherwise the filtered value is returned. - {{ variable | default('The variable is not defined') }} +```twig +{{ variable | default('The variable is not defined') }} - {{ variable.foo | default('The foo property on variable is not defined') }} +{{ variable.foo | default('The foo property on variable is not defined') }} - {{ variable['foo'] | default('The foo key in variable is not defined') }} +{{ variable['foo'] | default('The foo key in variable is not defined') }} - {{ '' | default('The variable is empty') }} +{{ '' | default('The variable is empty') }} +``` When using the `default` filter on an expression that uses variables in some method calls, be sure to use the `default` filter whenever a variable can be undefined: - {{ variable.method(foo | default('bar')) | default('bar') }} +```twig +{{ variable.method(foo | default('bar')) | default('bar') }} +``` diff --git a/markup-filter-image-height.md b/markup-filter-image-height.md index 2cb34018..085d1687 100644 --- a/markup-filter-image-height.md +++ b/markup-filter-image-height.md @@ -4,9 +4,11 @@ The `| imageHeight` filter attempts to identify the width in pixels of the provi > **NOTE:** This filter does not support thumbnail (already resized) versions of FileModels being passed as the image source. - {% set resizedImage = 'banner.jpg' | media | resize(1920, 1080) %} - +```twig +{% set resizedImage = 'banner.jpg' | media | resize(1920, 1080) %} + +``` See the [image resizing docs](../services/image-resizing#resize-sources) for more information on what image sources are supported. -> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. \ No newline at end of file +> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. diff --git a/markup-filter-image-width.md b/markup-filter-image-width.md index 63fe6b24..8fecd176 100644 --- a/markup-filter-image-width.md +++ b/markup-filter-image-width.md @@ -4,9 +4,11 @@ The `| imageWidth` filter attempts to identify the width in pixels of the provid > **NOTE:** This filter does not support thumbnail (already resized) versions of FileModels being passed as the image source. - {% set resizedImage = 'banner.jpg' | media | resize(1920, 1080) %} - +```twig +{% set resizedImage = 'banner.jpg' | media | resize(1920, 1080) %} + +``` See the [image resizing docs](../services/image-resizing#resize-sources) for more information on what image sources are supported. -> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. \ No newline at end of file +> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. diff --git a/markup-filter-md.md b/markup-filter-md.md index dda9c343..8e301575 100644 --- a/markup-filter-md.md +++ b/markup-filter-md.md @@ -2,8 +2,36 @@ The `| md` filter converts the value from Markdown to HTML format. - {{ '**Text** is bold.' | md }} +```twig +{{ '**Text** is bold.' | md }} +``` + +The above will output the following: + +```html +

Text is bold.

+``` + +# | md_line + +The `| md_line` filter converts the value from Markdown to HTML format, as inline element. + + {{ '**Text** is bold.' | md_line }} + +The above will output the following: + + Text is bold. + +# | md_safe + +The `| md_safe` filter converts the value from Markdown to HTML format, preventing `` blocks caused by indentation. + + {{ ' **Text** is bold.' | md_safe }} The above will output the following:

Text is bold.

+ +instead of + +
Text is bold.

\ No newline at end of file diff --git a/markup-filter-media.md b/markup-filter-media.md index 9f7fe733..1697c23c 100644 --- a/markup-filter-media.md +++ b/markup-filter-media.md @@ -2,8 +2,12 @@ The `| media` filter returns an address relative to the public path of the [media manager library](../cms/mediamanager). The result is a URL to the media file specified in the filter parameter. - +```twig + +``` If the media manager address is `https://cdn.wintercms.com` the above example would output the following: - +```html + +``` diff --git a/markup-filter-page.md b/markup-filter-page.md index 49ae7064..cabad7a7 100644 --- a/markup-filter-page.md +++ b/markup-filter-page.md @@ -2,90 +2,116 @@ The `| page` filter creates a link to a page using a page file name, without an extension, as a parameter. For example, if there is the about.htm page you can use the following code to generate a link to it: - About Us +```twig +About Us +``` Remember that if you refer a page from a subdirectory you should specify the subdirectory name: - About Us +```twig +About Us +``` > **NOTE**: The [Themes documentation](../cms/themes#subdirectories) has more details on subdirectory usage. To access the link to a certain page from the PHP section, you can use `$this->pageUrl('page-name-without-extension')`: - == - pageUrl('blog/overview'); - } - ?> - == - {{ newsPage }} +```php +== +pageUrl('blog/overview'); +} +?> +== +{{ newsPage }} +``` You can create a link to the current page by filtering an empty string: - Refresh page +```twig +Refresh page +``` To get the link to the current page in PHP, you can use `$this->pageUrl('')` with an empty string. - == - pageUrl(''); - } - ?> - == - {{ currentUrl }} +```php +== +pageUrl(''); +} +?> +== +{{ currentUrl }} +``` ## Reverse routing When linking to a page that has URL parameters defined, the ` | page` filter supports reverse routing by passing an array as the first argument. - url = "/blog/post/:post_id" - == - [...] +```ini +url = "/blog/post/:post_id" +== +[...] +``` Given the above content is found in a CMS page file **post.htm** you can link to this page using: - - Blog post #10 - +```twig + + Blog post #10 + +``` If the website address is __https://example.com__ the above example would output the following: - - Blog post #10 - +```html + + Blog post #10 + +``` ## Persistent URL parameters If a URL parameter is already presented in the environment, the ` | page` filter will use it automatically. - url = "/blog/post/:post_id" +```ini +url = "/blog/post/:post_id" - url = "/blog/post/edit/:post_id" +url = "/blog/post/edit/:post_id" +``` If there are two pages, **post.htm** and **post-edit.htm**, with the above URLs defined, you can link to either page without needing to define the `post_id` parameter. - - Edit this post - +```twig + + Edit this post + +``` When the above markup appears on the **post.htm** page, it will output the following: - - Edit this post - +```html + + Edit this post + +``` The `post_id` value of *10* is already known and has persisted across the environments. You can disable this functionality by passing the 2nd argument as `false`: - - Unknown blog post - +```twig + + Unknown blog post + +``` Or by defining a different value: - - Blog post #6 - +```twig + + Blog post #6 + +``` diff --git a/markup-filter-raw.md b/markup-filter-raw.md index dc871e9f..fd723404 100644 --- a/markup-filter-raw.md +++ b/markup-filter-raw.md @@ -2,18 +2,22 @@ Output variables in Winter are automatically escaped, the `| raw` filter marks the value as being "safe" and will not be escaped if `raw` is the last filter applied. - {# This variable won't be escaped #} - {{ variable | raw }} +```twig +{# This variable won't be escaped #} +{{ variable | raw }} +``` Be careful when using the `raw` filter inside expressions: - {% set hello = 'Hello' %} - {% set hola = 'Hola' %} +```twig +{% set hello = 'Hello' %} +{% set hola = 'Hola' %} - {{ false ? 'Hola' : hello | raw }} +{{ false ? 'Hola' : hello | raw }} - {# The above will not render the same as #} - {{ false ? hola : hello | raw }} +{# The above will not render the same as #} +{{ false ? hola : hello | raw }} - {# But renders the same as #} - {{ (false ? hola : hello) | raw }} +{# But renders the same as #} +{{ (false ? hola : hello) | raw }} +``` diff --git a/markup-filter-resize.md b/markup-filter-resize.md index 2427946a..c7918f03 100644 --- a/markup-filter-resize.md +++ b/markup-filter-resize.md @@ -2,7 +2,9 @@ The `| resize` filter attempts to resize the provided image source using the provided resizing configuration and outputs a URL to the resized image. - +```twig + +``` If the filter can successfully resize the provided image, then a URL to the Winter image resizer (`/resize/$id/$targetUrl`) will be rendered as the output of this filter until the image has been successfully resized. Once the image has been resized any future calls to this filter with the specific image and configuration combination will instead output a direct URL to the resized image. @@ -16,4 +18,4 @@ See the [image resizing docs](../services/image-resizing#resize-parameters) for - [List of locations images can be resized from](../services/image-resizing#resize-sources) -> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. \ No newline at end of file +> **NOTE:** The image resizing functionality requires a cache driver that persists cache data between requests in order to function, `array` is not a supported cache driver if you wish to use this functionality. diff --git a/markup-filter-theme.md b/markup-filter-theme.md index f7384dad..96d24acf 100644 --- a/markup-filter-theme.md +++ b/markup-filter-theme.md @@ -8,7 +8,7 @@ The `| theme` filter returns an address relative to the active theme path of the If the website address is __https://example.com__ and the active theme is called `website` the above example would output the following: -```twig +```html ``` @@ -19,4 +19,4 @@ The filter can also be used to interact with the [Asset Compiler](../services/as 'assets/css/styles1.css', 'assets/css/styles2.css' ] | theme }}" rel="stylesheet"> -``` \ No newline at end of file +``` diff --git a/markup-function-dump.md b/markup-function-dump.md index 473da17d..f4554138 100644 --- a/markup-function-dump.md +++ b/markup-function-dump.md @@ -2,12 +2,18 @@ The `dump()` function dumps information about a template variable. This is useful when debugging a template that does not behave as expected. - {{ dump(user) }} +```twig +{{ dump(user) }} +``` You can inspect several variables by passing them as additional arguments: - {{ dump(user, categories) }} +```twig +{{ dump(user, categories) }} +``` If you don't pass any value, all variables from the current context are dumped: - {{ dump() }} +```twig +{{ dump() }} +``` diff --git a/markup-function-form.md b/markup-function-form.md index 18d29aec..01ce8fdd 100644 --- a/markup-function-form.md +++ b/markup-function-form.md @@ -1,12 +1,16 @@ -# form() +# form_*() Functions prefixed with `form_` perform tasks that are useful when dealing with forms. The helper maps directly to the `Form` PHP class and its methods. For example: - {{ form_close() }} +```twig +{{ form_close() }} +``` is the PHP equivalent of the following: - +```php + +``` > **NOTE**: Methods in *camelCase* should be converted to *snake_case*. @@ -14,87 +18,125 @@ is the PHP equivalent of the following: Outputs a standard `` opening tag along with the `_session_key` and `_token` hidden fields for CSRF protection. If you are using the [AJAX Framework](../ajax/introduction), it is recommended that you use [`form_ajax()`](#form_ajax) instead. - {{ form_open() }} +```twig +{{ form_open() }} +``` Attributes can be passed in the first argument. - {{ form_open({ class: 'form-horizontal' }) }} +```twig +{{ form_open({ class: 'form-horizontal' }) }} +``` The above example would output as the following: - +```twig + +``` There are some special options that can also be used alongside the attributes. - {{ form_open({ request: 'onUpdate' }) }} +```twig +{{ form_open({ request: 'onUpdate' }) }} +``` The function support the following options: + +
+ Option | Description ------------- | ------------- -**method** | Request method. Corresponds to the **method** FORM tag attribute. Eg: POST, GET, PUT, DELETE -**request** | A handler name to execute on the server when the form is posted. See the [Handling forms](../cms/pages#handling-forms) article for details about the event handlers. -**url** | Specifies URL to post the form to. Corresponds to the **action** FORM tag attribute. -**files** | Determines whether the form will submit files. Accepted values: **true** and **false**. -**model** | A model object for the form model binding. +`method` | Request method. Corresponds to the `method` FORM tag attribute. Eg: `POST`, `GET`, `PUT`, `DELETE` +`request` | A handler name to execute on the server when the form is posted. See the [Handling forms](../cms/pages#handling-forms) article for details about the event handlers. +`url` | Specifies URL to post the form to. Corresponds to the `action` FORM tag attribute. +`files` | Determines whether the form will submit files. Accepted values: `true` and `false`. +`model` | A model object for the form model binding. ## form_ajax() Outputs an AJAX enabled FORM opening tag. The first parameter of the `form_ajax()` function is the AJAX handler name. The handler can be defined in the layout or page [PHP section](../cms/themes#php-section) code, it can also be defined in a component. You may find more information about AJAX in the [AJAX Framework](../ajax/introduction) article. - {{ form_ajax('onUpdate') }} +```twig +{{ form_ajax('onUpdate') }} +``` Attributes can be passed in the second argument. - {{ form_ajax('onSave', { class: 'form-horizontal'}) }} +```twig +{{ form_ajax('onSave', { class: 'form-horizontal'}) }} +``` The above example would output as the following: - +```html + +``` There are some special options that can also be used alongside the attributes. - {{ form_ajax('onDelete', { data: { id: 2 }, confirm: 'Really delete this record?' }) }} +```twig +{{ form_ajax('onDelete', { data: { id: 2 }, confirm: 'Really delete this record?' }) }} - {{ form_ajax('onRefresh', { update: { statistics: '#statsPanel' } }) }} +{{ form_ajax('onRefresh', { update: { statistics: '#statsPanel' } }) }} +``` > **NOTE:** When attempting to reference a component's alias with `__SELF__` as an argument to `form_ajax()` you must first build the string you wish to use outside of the call itself. Example: - {% set targetPartial = "'" ~ __SELF__ ~ "::statistics': '#statsPanel'" %} - {{ form_ajax('onUpdate', { update: targetPartial }) }} +```twig +{% set targetPartial = "'" ~ __SELF__ ~ "::statistics': '#statsPanel'" %} +{{ form_ajax('onUpdate', { update: targetPartial }) }} +``` The function support the following options: + +
+ Option | Description ------------- | ------------- -**success** | JavaScript string to execute on successful result. -**error** | JavaScript string to execute on failed result. -**confirm** | A confirmation message to display before sending the request. -**redirect** | On successful result, redirect to a URL. -**update** | An array of partials to update on success in the following format: { 'partial': '#element' }. -**data** | Extra data to include with the request in the following format: { 'myvar': 'myvalue' }. +`success` | JavaScript string to execute on successful result. +`error` | JavaScript string to execute on failed result. +`confirm` | A confirmation message to display before sending the request. +`redirect` | On successful result, redirect to a URL. +`update` | An array of partials to update on success in the following format: `{ 'partial': '#element' }`. +`data` | Extra data to include with the request in the following format: `{ 'myvar': 'myvalue' }`. ## form_close() Outputs a standard FORM closing tag. This tag is generally available to provide consistency in usage. - {{ form_close() }} +```twig +{{ form_close() }} +``` The above example would output as the following: - +```html + +``` ## Passing attributes to the generated element You can pass additional attributes to the `Form::open()` method by passing an array of attribute names and values to be rendered on the final generated `
` element. - 'example', 'class' => 'something')) ?> - // .. - +```php + 'example', 'class' => 'something')) ?> + // .. + +``` The above example would output the following: - +```html + -
+ +``` diff --git a/markup-function-html.md b/markup-function-html.md index 1eaae667..0ab20602 100644 --- a/markup-function-html.md +++ b/markup-function-html.md @@ -2,11 +2,15 @@ Functions prefixed with `html_` perform tasks that are useful when dealing with html markup. The helper maps directly to the `Html` PHP class and its methods. For example: - {{ html_strip() }} +```twig +{{ html_strip() }} +``` is the PHP equivalent of the following: - +```php + +``` > **NOTE**: Methods in *camelCase* should be converted to *snake_case*. @@ -14,33 +18,45 @@ is the PHP equivalent of the following: Removes HTML from a string. - {{ html_strip('Hello world') }} +```twig +{{ html_strip('Hello world') }} +``` ## html_limit() Limits HTML with specific length with a proper tag handling. - {{ html_limit('

Post content...

', 100) }} +```twig +{{ html_limit('

Post content...

', 100) }} +``` To add a suffix when limit is applied, pass it as the third argument. Defaults to `...`. - {{ html_limit('

Post content...

', 100, '... Read more!') }} +```twig +{{ html_limit('

Post content...

', 100, '... Read more!') }} +``` ## html_clean() Cleans HTML to prevent most XSS attacks. - {{ html_clean('') }} +```twig +{{ html_clean('') }} +``` ## html_email() Obfuscates an e-mail address to prevent spam-bots from sniffing it. - {{ html_email('a@b.c') }} +```twig +{{ html_email('a@b.c') }} +``` For example: - Email me +```twig +Email me - - Email me + +Email me +``` diff --git a/markup-function-str.md b/markup-function-str.md index 5e13333d..c7548467 100644 --- a/markup-function-str.md +++ b/markup-function-str.md @@ -1,64 +1,19 @@ -# str() +# str_*() Functions prefixed with `str_` perform tasks that are useful when dealing with strings. The helper maps directly to the `Str` PHP class and its methods. For example: - {{ str_camel() }} +```twig +{{ str_camel() }} +``` is the PHP equivalent of the following: - +```php + +``` > **NOTE**: Methods in *camelCase* should be converted to *snake_case*. -## str_limit() +See [Helpers#helpers-string](../services/helpers#helpers-strings) for a list of all available `str_*` helpers. -Limit the number of characters in a string. - - {{ str_limit('The quick brown fox...', 100) }} - -To add a suffix when limit is applied, pass it as the third argument. Defaults to `...`. - - {{ str_limit('The quick brown fox...', 100, '... Read more!') }} - -## str_words() - -Limit the number of words in a string. - - {{ str_words('The quick brown fox...', 100) }} - -To add a suffix when limit is applied, pass it as the third argument. Defaults to `...`. - - {{ str_words('The quick brown fox...', 100, '... Read more!') }} - -## str_camel() - -Convert a value to *camelCase*. - - // Outputs: helloWorld - {{ str_camel('hello world') }} - -## str_studly() - -Convert a value to *StudlyCase*. - - // Outputs: HelloWorld - {{ str_studly('hello world') }} - -## str_snake() - -Convert a value to *snake_case*. - - // Outputs: hello_world - {{ str_snake('hello world') }} - -The second argument can supply a delimiter. - - // Outputs: hello---world - {{ str_snake('hello world', '---') }} - -## str_plural() - -Get the plural form of an English word. - - // Outputs: chickens - {{ str_plural('chicken') }} +See [Laravel Helpers](https://laravel.com/docs/6.x/helpers#available-methods) for a list of all available `str_*` helpers that come from Laravel. Any helper that matches `Str::$camelCase` is available in Twig as `str_$snake_case` with the same parameters \ No newline at end of file diff --git a/markup-tag-component.md b/markup-tag-component.md index 4a3e1e3a..465005f9 100644 --- a/markup-tag-component.md +++ b/markup-tag-component.md @@ -2,18 +2,24 @@ The `{% component %}` tag will parse the default markup content for a [CMS component](../cms/components) and display it on the page. Not all components provide default markup, the documentation for the plugin will guide in the correct usage. - {% component "blogPosts" %} +```twig +{% component "blogPosts" %} +``` This will render the component partial with a fixed name of **default.htm** and is essentially an alias for the following: - {% partial "blogPosts::default" %} +```twig +{% partial "blogPosts::default" %} +``` ## Variables Some components support [passing variables](../cms/components#component-variables) at render time: - {% component "blogPosts" postsPerPage="5" %} +```twig +{% component "blogPosts" postsPerPage="5" %} +``` ## Customizing components diff --git a/markup-tag-content.md b/markup-tag-content.md index 9ad819c3..33a067ab 100644 --- a/markup-tag-content.md +++ b/markup-tag-content.md @@ -2,57 +2,77 @@ The `{% content %}` tag will display a [CMS content block](../cms/content) on the page. To display content block called **contacts.htm** you pass the file name after the `content` tag quoted as a string. - {% content "contacts.htm" %} +```twig +{% content "contacts.htm" %} +``` A content block inside a subdirectory can be rendered in the same way. - {% content "sidebar/content.htm" %} +```twig +{% content "sidebar/content.htm" %} +``` > **NOTE**: The [Themes documentation](../cms/themes#subdirectories) has more details on subdirectory usage. Content blocks can be rendered as plain text: - {% content "readme.txt" %} +```twig +{% content "readme.txt" %} +``` You can also use Markdown syntax: - {% content "changelog.md" %} +```twig +{% content "changelog.md" %} +``` Content blocks can also be used in combination with [layout placeholders](../cms/layouts#placeholders): - {% put sidebar %} - {% content 'sidebar-content.htm' %} - {% endput %} +```twig +{% put sidebar %} + {% content 'sidebar-content.htm' %} +{% endput %} +``` ## Variables You can pass variables to content blocks by specifying them after the file name: - {% content "welcome.htm" name=user.name %} +```twig +{% content "welcome.htm" name=user.name %} +``` You can also assign new variables for use in the content: - {% content "location.htm" city="Vancouver" country="Canada" %} +```twig +{% content "location.htm" city="Vancouver" country="Canada" %} +``` Inside the content, variables can be accessed using a basic syntax using singular *curly brackets*: -

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

+```twig +

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

+``` You can also pass a collection of variables as a simple array: - {% content "welcome.htm" likes=[ - {name:'Dogs'}, - {name:'Fishing'}, - {name:'Golf'} - ] %} +```twig +{% content "welcome.htm" likes=[ + {name:'Dogs'}, + {name:'Fishing'}, + {name:'Golf'} +] %} +``` The collection of variables is accessed by using an opening and closing set of brackets: -
    - {likes} -
  • {name}
  • - {/likes} -
+```twig +
    + {likes} +
  • {name}
  • + {/likes} +
+``` > **NOTE**: Twig syntax is not supported in Content blocks, consider using a [CMS partial](../cms/partials) instead. diff --git a/markup-tag-flash.md b/markup-tag-flash.md index 467f0c0d..3a6e5440 100644 --- a/markup-tag-flash.md +++ b/markup-tag-flash.md @@ -2,43 +2,51 @@ The `{% flash %}` and `{% endflash %}` tags will render any flash messages stored in the user session, set by the `Flash` PHP class. The `message` variable inside will contain the flash message text and the markup inside will repeat for multiple flash messages. -
    - {% flash %} -
  • {{ message }}
  • - {% endflash %} -
- -You can use the `type` variable that represents the flash message type — **success**, **error**, **info** or **warning**. - +```twig +
    {% flash %} -
    - {{ message }} -
    +
  • {{ message }}
  • {% endflash %} +
+``` -You can also specify the `type` to filter flash messages of a given type. The next example will show only **success** messages, if there is an **error** message it won't be displayed. +You can use the `type` variable that represents the flash message type — `success`, `error`, `info` or `warning`. - {% flash success %} -
{{ message }}
- {% endflash %} +```twig +{% flash %} +
+ {{ message }} +
+{% endflash %} +``` + +You can also specify the `type` to filter flash messages of a given type. The next example will show only `success` messages, if there is an `error` message it won't be displayed. + +```twig +{% flash success %} +
{{ message }}
+{% endflash %} +``` ## Setting flash messages Flash messages can be set by [Components](../cms/components) or inside the page or layout [PHP section](../cms/themes#php-section) with the `Flash` class. - - {% for user in users %} -
  • {{ user.username }}
  • - {% endfor %} - +```twig +
      + {% for user in users %} +
    • {{ user.username }}
    • + {% endfor %} +
    +``` You can also access both keys and values: -
      - {% for key, user in users %} -
    • {{ key }}: {{ user.username }}
    • - {% endfor %} -
    +```twig +
      + {% for key, user in users %} +
    • {{ key }}: {{ user.username }}
    • + {% endfor %} +
    +``` If the collection is empty, you can render a replacement block by using else: -
      - {% for user in users %} -
    • {{ user.username }}
    • - {% else %} -
    • There are no users found
    • - {% endfor %} -
    +```twig +
      + {% for user in users %} +
    • {{ user.username }}
    • + {% else %} +
    • There are no users found
    • + {% endfor %} +
    +``` ## Looping a collection If you do need to iterate over a collection of numbers, you can use the `..` operator: - {% for i in 0..10 %} - - {{ i }} - {% endfor %} +```twig +{% for i in 0..10 %} + - {{ i }} +{% endfor %} +``` The above snippet of code would print all numbers from 0 to 10. It can also be useful with letters: - {% for letter in 'a'..'z' %} - - {{ letter }} - {% endfor %} +```twig +{% for letter in 'a'..'z' %} + - {{ letter }} +{% endfor %} +``` The `..` operator can take any expression at both sides: - {% for letter in 'a'|upper..'z'|upper %} - - {{ letter }} - {% endfor %} +```twig +{% for letter in 'a' | upper..'z' | upper %} + - {{ letter }} +{% endfor %} +``` ## Adding a condition Unlike in PHP there is no function to `break` or `continue` in a loop, however you can still filter the collection. The following example skips all the `users` which are not active: -
      - {% for user in users if user.active %} -
    • {{ user.username }}
    • - {% endfor %} -
    +```twig +
      + {% for user in users if user.active %} +
    • {{ user.username }}
    • + {% endfor %} +
    +``` ## The loop variable Inside of a `for` loop block you can access some special variables: + +
    + Variable | Description ------------- | ------------- `loop.index` | The current iteration of the loop. (1 indexed) @@ -73,6 +93,8 @@ Variable | Description `loop.length` | The number of items in the collection `loop.parent` | The parent context - {% for user in users %} - {{ loop.index }} - {{ user.username }} - {% endfor %} +```twig +{% for user in users %} + {{ loop.index }} - {{ user.username }} +{% endfor %} +``` diff --git a/markup-tag-if.md b/markup-tag-if.md index 43af747d..771ce001 100644 --- a/markup-tag-if.md +++ b/markup-tag-if.md @@ -2,37 +2,45 @@ The `{% if %}` and `{% endif %}` tags will represent an expression and is comparable with the if statements of PHP. In the simplest form you can use it to test if an expression evaluates to `true`: - {% if online == false %} -

    The website is in maintenance mode.

    - {% endif %} +```twig +{% if online == false %} +

    The website is in maintenance mode.

    +{% endif %} +``` You can also test if an array is not empty: - {% if users %} -
      - {% for user in users %} -
    • {{ user.username }}
    • - {% endfor %} -
    - {% endif %} +```twig +{% if users %} +
      + {% for user in users %} +
    • {{ user.username }}
    • + {% endfor %} +
    +{% endif %} +``` > **NOTE**: If you want to test if the variable is defined, use `{% if users is defined %}` instead. You can also use `not` to check for values that evaluate to `false`: - {% if not user.subscribed %} -

    You are not subscribed to our mailing list.

    - {% endif %} +```twig +{% if not user.subscribed %} +

    You are not subscribed to our mailing list.

    +{% endif %} +``` For multiple expressions `{% elseif %}` and `{% else %}` can be used: - {% if kenny.sick %} - Kenny is sick. - {% elseif kenny.dead %} - You killed Kenny! You bastard!!! - {% else %} - Kenny looks okay so far. - {% endif %} +```twig +{% if kenny.sick %} + Kenny is sick. +{% elseif kenny.dead %} + You killed Kenny! You bastard!!! +{% else %} + Kenny looks okay so far. +{% endif %} +``` ## Expression rules @@ -40,10 +48,10 @@ The rules to determine if an expression is true or false are the same as in PHP, Value | Boolean evaluation ------------- | ------------- -*empty string* | false -*numeric zero* | false -*whitespace-only string* | true -*empty array* | false -*null* | false -*non-empty array* | true -*object* | true +*empty string* | `false` +*numeric zero* | `false` +*whitespace-only string* | `true` +*empty array* | `false` +*null* | `false` +*non-empty array* | `true` +*object* | `true` diff --git a/markup-tag-macro.md b/markup-tag-macro.md index adfcf6c4..5c7b5d7a 100644 --- a/markup-tag-macro.md +++ b/markup-tag-macro.md @@ -2,25 +2,31 @@ The `{% macro %}` tag allows you to define custom functions in your templates, similar to regular programming languages. - {% macro input() %} - ... - {% endmacro %} +```twig +{% macro input() %} + ... +{% endmacro %} +``` Alternatively you can include the name of the macro after the end tag for better readability: - {% macro input() %} - ... - {% endmacro input %} +```twig +{% macro input() %} + ... +{% endmacro input %} +``` The following example defines a function called `input()` that takes 4 arguments, the associated values are accessed as variables within the markup inside. - {% macro input(name, value, type, size) %} - - {% endmacro %} +```twig +{% macro input(name, value, type, size) %} + +{% endmacro %} +``` > **NOTE**: Macro arguments don't specify default values and are always considered optional. @@ -29,62 +35,76 @@ The following example defines a function called `input()` that takes 4 arguments Before a macro can be used it needs to be "imported" first using the `{% import %}` tag. If the macro is defined in the same template, the special `_self` variable can be used. - {% import _self as form %} +```twig +{% import _self as form %} +``` Here the macro functions are assigned to the `form` variable, available to be called like any other function. -

    {{ form.input('username') }}

    -

    {{ form.input('password', null, 'password') }}

    +```twig +

    {{ form.input('username') }}

    +

    {{ form.input('password', null, 'password') }}

    +``` Macros can be defined in [a theme partial](../cms/partials) and imported by name. To import the macros from a partial called **macros/form.htm**, simply pass the name after the `import` tag quoted as a string. - {% import 'macros/form' as form %} +```twig +{% import 'macros/form' as form %} +``` Alternatively you may import macros from a [system view file](../services/response-view#views) and these will be accepted. To import from **plugins/acme/blog/views/macros.htm** simply pass the path hint instead. - {% import 'acme.blog::macros' as form %} +```twig +{% import 'acme.blog::macros' as form %} +``` ## Nested macros When you want to use a macro inside another macro from the same template, you need to import it locally. - {% macro input(name, value, type, size) %} - - {% endmacro %} +```twig +{% macro input(name, value, type, size) %} + +{% endmacro %} - {% macro wrapped_input(name, value, type, size) %} - {% import _self as form %} +{% macro wrapped_input(name, value, type, size) %} + {% import _self as form %} -
    - {{ form.input(name, value, type, size) }} -
    - {% endmacro %} +
    + {{ form.input(name, value, type, size) }} +
    +{% endmacro %} +``` ## Context variable Macros don't have access to the current page variables. - - {{ site_name }} +```twig + +{{ site_name }} - {% macro myFunction() %} - - {{ site_name }} - {% endmacro %} +{% macro myFunction() %} + + {{ site_name }} +{% endmacro %} +``` You may pass the variables to the function using the special `_context` variable. - {% macro myFunction(vars) %} - {{ vars.site_name }} - {% endmacro %} +```twig +{% macro myFunction(vars) %} + {{ vars.site_name }} +{% endmacro %} - {% import _self as form %} +{% import _self as form %} - - {{ form.myFunction(_context) }} + +{{ form.myFunction(_context) }} +``` diff --git a/markup-tag-page.md b/markup-tag-page.md index 77314108..f223e29d 100644 --- a/markup-tag-page.md +++ b/markup-tag-page.md @@ -6,31 +6,36 @@ See [layouts](../cms/layouts#introduction) for a basic example. The `{% page %}` tag parses the raw markup from a page template. A page template may inject content both into placeholder(s) as well as define raw markup. - description="example layout" - == - - - {% placeholder head %} - - - {% page %} - ... - - description="example page" - == - {% put head %} - - {% endput %} - -

    My content.

    - +```twig +description="example layout" +== + + + {% placeholder head %} + + + {% page %} + ... +``` + +```twig +description="example page" +== +{% put head %} + +{% endput %} + +

    My content.

    +``` + The page rendered with the template would result in: - - - - - -

    My content.

    - ... - \ No newline at end of file +```html + + + + + +

    My content.

    + ... +``` diff --git a/markup-tag-partial.md b/markup-tag-partial.md index bd97ca9a..552985af 100644 --- a/markup-tag-partial.md +++ b/markup-tag-partial.md @@ -2,43 +2,57 @@ The `{% partial %}` tag will parse a [CMS partial](../cms/partials) and render the partial contents on the page. To display a partial called **footer.htm**, simply pass the name after the `partial` tag quoted as a string. - {% partial "footer" %} +```twig +{% partial "footer" %} +``` A partial inside a subdirectory can be rendered in the same way. - {% partial "sidebar/menu" %} +```twig +{% partial "sidebar/menu" %} +``` > **NOTE**: The [Themes documentation](../cms/themes#subdirectories) has more details on subdirectory usage. The partial name can also be a variable: - {% set tabName = "profile" %} - {% partial tabName %} +```twig +{% set tabName = "profile" %} +{% partial tabName %} +``` ## Variables You can pass variables to partials by specifying them after the partial name: - {% partial "blog-posts" posts=posts %} +```twig +{% partial "blog-posts" posts=posts %} +``` You can also assign new variables for use in the partial: - {% partial "location" city="Vancouver" country="Canada" %} +```twig +{% partial "location" city="Vancouver" country="Canada" %} +``` Inside the partial, variables can be accessed like any other markup variable: -

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

    +```twig +

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

    +``` ## Checking a partial exists In any template you can check if a partial content exists by using the `partial()` function. This lets you to generate different markup depending on whether the partial exists or not. Example: - {% set cardPartial = 'my-cards/' ~ cardCode %} +```twig +{% set cardPartial = 'my-cards/' ~ cardCode %} - {% if partial(cardPartial) %} - {% partial cardPartial %} - {% else %} -

    Card not found!

    - {% endif %} +{% if partial(cardPartial) %} + {% partial cardPartial %} +{% else %} +

    Card not found!

    +{% endif %} +``` diff --git a/markup-tag-placeholder.md b/markup-tag-placeholder.md index effb7a09..7d2fa21d 100644 --- a/markup-tag-placeholder.md +++ b/markup-tag-placeholder.md @@ -2,59 +2,73 @@ The `{% placeholder %}` tag will render a placeholder section which is generally [used inside Layouts](../cms/layouts#placeholders). This tag will return any placeholder contents that have been added using the `{% put %}` tag, or any default content that is defined (optional). - {% placeholder name %} +```twig +{% placeholder name %} +``` Content can then be injected into the placeholder in any subsequent page or partial. - {% put name %} -

    Place this text in the name placeholder

    - {% endput %} +```twig +{% put name %} +

    Place this text in the name placeholder

    +{% endput %} +``` ## Default placeholder content Placeholders can have default content that can be either replaced or complemented by a page. If the `{% put %}` tag for a placeholder with default content is not defined on a page, the default placeholder content is displayed. Example placeholder definition in the layout template: - {% placeholder sidebar default %} -

    Contact us

    - {% endplaceholder %} +```twig +{% placeholder sidebar default %} +

    Contact us

    +{% endplaceholder %} +``` The page can inject more content to the placeholder. The `{% default %}` tag specifies a place where the default placeholder content should be displayed. If the tag is not used the placeholder content is completely replaced. - {% put sidebar %} -

    Services

    - {% default %} - {% endput %} +```twig +{% put sidebar %} +

    Services

    + {% default %} +{% endput %} +``` ## Checking a placeholder exists In a layout template you can check if a placeholder content exists by using the `placeholder()` function. This lets you to generate different markup depending on whether the page provides a placeholder content. Example: - {% if placeholder('sidemenu') %} - -
    -
    - {% placeholder sidemenu %} -
    -
    - {% page %} -
    +```twig +{% if placeholder('sidemenu') %} + +
    +
    + {% placeholder sidemenu %}
    - {% else %} - - {% page %} - {% endif %} +
    + {% page %} +
    +
    +{% else %} + + {% page %} +{% endif %} +``` ## Custom attributes The `placeholder` tag accepts two optional attributes — `title` and `type`. The `title` attribute is not used by the CMS itself, but could be used by other plugins. The type attribute manages the placeholder type. There are two types supported at the moment — **text** and **html**. The content of text placeholders is escaped before it's displayed. The title and type attributes should be defined after the placeholder name and the `default` attribute, if it's presented. Example: - {% placeholder ordering title="Ordering information" type="text" %} +```twig +{% placeholder ordering title="Ordering information" type="text" %} +``` Example of a placeholder with a default content, title and type attributes. - {% placeholder ordering default title="Ordering information" type="text" %} - There is no ordering information for this product. - {% endplaceholder %} +```twig +{% placeholder ordering default title="Ordering information" type="text" %} + There is no ordering information for this product. +{% endplaceholder %} +``` diff --git a/markup-tag-verbatim.md b/markup-tag-verbatim.md index 39969dda..a7dd7e09 100644 --- a/markup-tag-verbatim.md +++ b/markup-tag-verbatim.md @@ -2,16 +2,22 @@ The `{% verbatim %}` tag marks entire sections as being raw text that should not be parsed. - {% verbatim %}

    Hello, {{ name }}

    {% endverbatim %} +```twig +{% verbatim %}

    Hello, {{ name }}

    {% endverbatim %} +``` The above will render in the browser exactly as: -

    Hello, {{ name }}

    +```twig +

    Hello, {{ name }}

    +``` For example, AngularJS uses the same templating syntax so you can decide which variables to use for each. -

    Hello {{ name }}, this is parsed by Twig

    +```twig +

    Hello {{ name }}, this is parsed by Twig

    - {% verbatim %} -

    Hello {{ name }}, this is parsed by AngularJS

    - {% endverbatim %} +{% verbatim %} +

    Hello {{ name }}, this is parsed by AngularJS

    +{% endverbatim %} +``` diff --git a/markup-templating.md b/markup-templating.md index a0047c18..b3977271 100644 --- a/markup-templating.md +++ b/markup-templating.md @@ -6,15 +6,21 @@ Winter extends the [Twig template language](https://twig.symfony.com/doc/2.x/) w Template variables are printed on the page using *double curly brackets*. - {{ variable }} +```twig +{{ variable }} +``` Variables can also represent *expressions*. - {{ isAjax ? 'Yes' : 'No' }} +```twig +{{ isAjax ? 'Yes' : 'No' }} +``` Variables can be concatenated with the `~` character. - {{ 'Your name: ' ~ name }} +```twig +{{ 'Your name: ' ~ name }} +``` Winter provides global variables under the `this` variable, as listed under the **Variables** section. @@ -22,19 +28,25 @@ Winter provides global variables under the `this` variable, as listed under the Tags are a unique feature to Twig and are wrapped with `{% %}` characters. - {% tag %} +```twig +{% tag %} +``` Tags provide a more fluent way to describe template logic. - {% if stormCloudComing %} - Stay inside - {% else %} - Go outside and play - {% endif %} +```twig +{% if stormCloudComing %} + Stay inside +{% else %} + Go outside and play +{% endif %} +``` The `{% set %}` tag can be used to set variables inside the template. - {% set activePage = 'blog' %} +```twig +{% set activePage = 'blog' %} +``` Tags can take on many different syntaxes and are listed under the **Tags** section. @@ -42,15 +54,21 @@ Tags can take on many different syntaxes and are listed under the **Tags** secti Filters act as modifiers to variables for a single instance and are applied using a *pipe symbol* followed by the filter name. - {{ 'string'|filter }} +```twig +{{ 'string' | filter }} +``` Filters can take arguments like a function. - {{ price|currency('USD') }} +```twig +{{ price | currency('USD') }} +``` Filters can be applied in succession. - {{ 'Winter Glory'|upper|replace({'Winter': 'Morning'}) }} +```twig +{{ 'Winter Glory' | upper | replace({'Winter': 'Morning'}) }} +``` Filters are listed under the **Filters** section. @@ -58,11 +76,15 @@ Filters are listed under the **Filters** section. Functions allow logic to be executed and the return result acts as a variable. - {{ function() }} +```twig +{{ function() }} +``` Functions can take arguments. - {{ dump(variable) }} +```twig +{{ dump(variable) }} +``` Functions are listed under the **Functions** section. diff --git a/markup-this-environment.md b/markup-this-environment.md index dfc98c62..b2f33d5e 100644 --- a/markup-this-environment.md +++ b/markup-this-environment.md @@ -6,8 +6,10 @@ You can access the current environment object via `this.environment` and it retu The following example will display a banner if the website is running in the test environment: - {% if this.environment == 'test' %} +```twig +{% if this.environment == 'test' %} - + - {% endif %} +{% endif %} +``` diff --git a/markup-this-layout.md b/markup-this-layout.md index b4b44ecb..bf24d2a8 100644 --- a/markup-this-layout.md +++ b/markup-this-layout.md @@ -10,7 +10,9 @@ You can access the current layout object via `this.layout` and it returns the ob Converts the layout file name and folder name to a CSS friendly identifier. - +```twig + +``` If the layout file was **default.htm** this would generate a class name of `layout-default`. @@ -18,4 +20,6 @@ If the layout file was **default.htm** this would generate a class name of `layo The layout description as defined by the configuration. - +```twig + +``` diff --git a/markup-this-page.md b/markup-this-page.md index 4fdbbfda..41ddc2d4 100644 --- a/markup-this-page.md +++ b/markup-this-page.md @@ -10,13 +10,17 @@ You can access the current page object via `this.page` and it returns the object Reference to the layout name used by this page, if defined. Not to be confused with `this.layout`. - {{ this.page.layout }} +```twig +{{ this.page.layout }} +``` ### id Converts the page file name and folder name to a CSS friendly identifier. - +```twig + +``` If the page file was **home/index.htm** this would generate a class name of `page-home-index`. @@ -24,33 +28,43 @@ If the page file was **home/index.htm** this would generate a class name of `pag The page title as defined by the configuration. -

    {{ this.page.title }}

    +```twig +

    {{ this.page.title }}

    +``` ### description The page description as defined by the configuration. -

    {{ this.page.description }}

    +```twig +

    {{ this.page.description }}

    +``` ### meta_title An alternative `title` field, usually more descriptive for SEO purposes. - {{ this.page.meta_title }} +```twig +{{ this.page.meta_title }} +``` ### meta_description An alternative `description` field, usually more descriptive for SEO purposes. - +```twig + +``` ### hidden Hidden pages are accessible only by logged-in backend users. - {% if this.page.hidden %} -

    Note to other admins: We are currently working on this page.

    - {% endif %} +```twig +{% if this.page.hidden %} +

    Note to other admins: We are currently working on this page.

    +{% endif %} +``` ### fileName diff --git a/markup-this-param.md b/markup-this-param.md index 925d37cc..e21d0c86 100644 --- a/markup-this-param.md +++ b/markup-this-param.md @@ -6,22 +6,26 @@ You can access the current URL parameters via `this.param` and it returns a PHP This example demonstrates how to access the `tab` URL parameter in a page. - url = "/account/:tab" - == - {% if this.param.tab == 'details' %} +```twig +url = "/account/:tab" +== +{% if this.param.tab == 'details' %} -

    Here are all your details

    +

    Here are all your details

    - {% elseif this.param.tab == 'history' %} +{% elseif this.param.tab == 'history' %} -

    You are viewing a blast from the past

    +

    You are viewing a blast from the past

    - {% endif %} +{% endif %} +``` If the parameter name is also a variable, then array syntax can be used. - url = "/account/:post_id" - == - {% set name = 'post_id' %} +```twig +url = "/account/:post_id" +== +{% set name = 'post_id' %} -

    The post ID is: {{ this.param[name] }}

    +

    The post ID is: {{ this.param[name] }}

    +``` diff --git a/markup-this-session.md b/markup-this-session.md index 3f5e0a0a..490368f7 100644 --- a/markup-this-session.md +++ b/markup-this-session.md @@ -4,26 +4,34 @@ You can access the current session manager via `this.session` and it returns the ## Storing data in the session - {{ this.session.put('key', 'value') }} +```twig +{{ this.session.put('key', 'value') }} +``` ## Retrieving data from the session - {{ this.session.get('key') }} +```twig +{{ this.session.get('key') }} +``` ## Determining if an item exists in the session - {% if this.session.has('key') %} -

    we found it in the session

    - {% endif %} +```twig +{% if this.session.has('key') %} +

    we found it in the session

    +{% endif %} +``` ## Deleting data from the session #### Remove data for a single key - {{ this.session.forget('key') }} +```twig +{{ this.session.forget('key') }} +``` #### Remove all session data - {{ this.session.flush() }} - - +```twig +{{ this.session.flush() }} +``` diff --git a/markup-this-theme.md b/markup-this-theme.md index 0f31beba..0b75ad5b 100644 --- a/markup-this-theme.md +++ b/markup-this-theme.md @@ -10,7 +10,9 @@ You can access the current theme object via `this.theme` and it returns the obje Converts the theme directory name to a CSS friendly identifier. - +```twig + +``` If the theme directory was **website** this would generate a class name of `theme-website`. @@ -18,4 +20,6 @@ If the theme directory was **website** this would generate a class name of `them An array containing all the theme configuration values found in the `theme.yaml` file. - +```twig + +``` diff --git a/plugin-components.md b/plugin-components.md index c969ac38..3c68ba34 100644 --- a/plugin-components.md +++ b/plugin-components.md @@ -24,14 +24,18 @@ Components files and directories reside in the **/components** subdirectory of a plugin directory. Each component has a PHP file defining the component class and an optional component partials directory. The component partials directory name matches the component class name written in lowercase. An example of a component directory structure: - plugins/ - acme/ - myplugin/ - components/ - componentname/ <=== Component partials directory - default.htm <=== Component default markup (optional) - ComponentName.php <=== Component class file - Plugin.php +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ myplugin + ┣ πŸ“‚ components + ┃ ┣ πŸ“‚ componentname <=== Component partials directory + ┃ ┃ β”— πŸ“œ default.htm <=== Component default markup (optional) + ┃ ┣ πŸ“‚ partials <=== Any partials shared by more than one component in the plugin + ┃ ┃ β”— πŸ“œ partialname.htm + ┃ β”— πŸ“œ ComponentName.php <=== Component class file + β”— πŸ“œ Plugin.php +``` Components must be [registered in the Plugin registration class](#component-registration) with the `registerComponents` method. @@ -40,51 +44,59 @@ Components must be [registered in the Plugin registration class](#component-regi The **component class file** defines the component functionality and [component properties](#component-properties). The component class file name should match the component class name. Component classes should extend the `\Cms\Classes\ComponentBase` class. The component from the next example should be defined in the plugins/acme/blog/components/BlogPosts.php file. - namespace Acme\Blog\Components; +```php +namespace Acme\Blog\Components; + +class BlogPosts extends \Cms\Classes\ComponentBase +{ + public function componentDetails() + { + return [ + 'name' => 'Blog Posts', + 'description' => 'Displays a collection of blog posts.' + ]; + } - class BlogPosts extends \Cms\Classes\ComponentBase + // This array becomes available on the page as {{ component.posts }} + public function posts() { - public function componentDetails() - { - return [ - 'name' => 'Blog Posts', - 'description' => 'Displays a collection of blog posts.' - ]; - } - - // This array becomes available on the page as {{ component.posts }} - public function posts() - { - return ['First Post', 'Second Post', 'Third Post']; - } + return ['First Post', 'Second Post', 'Third Post']; } +} +``` The `componentDetails` method is required. The method should return an array with two keys: `name` and `description`. The name and description are display in the CMS backend user interface. When this [component is attached to a page or layout](../cms/components), the class properties and methods become available on the page through the component variable, which name matches the component short name or the alias. For example, if the BlogPost component from the previous example was defined on a page with its short name: - url = "/blog" +```ini +url = "/blog" - [blogPosts] - == +[blogPosts] +== +``` You would be able to access its `posts` method through the `blogPosts` variable. Note that Twig supports the property notation for methods, so that you don't need to use brackets. - {% for post in blogPosts.posts %} - {{ post }} - {% endfor %} +```twig +{% for post in blogPosts.posts %} + {{ post }} +{% endfor %} +``` ### Component registration Components must be registered by overriding the `registerComponents` method inside the [Plugin registration class](registration#registration-file). This tells the CMS about the Component and provides a **short name** for using it. An example of registering a component: - public function registerComponents() - { - return [ - 'Winter\Demo\Components\Todo' => 'demoTodo' - ]; - } +```php +public function registerComponents() +{ + return [ + 'Winter\Demo\Components\Todo' => 'demoTodo' + ]; +} +``` This will register the Todo component class with the default alias name **demoTodo**. More information on using components can be found at the [CMS components article](../cms/components). @@ -93,188 +105,238 @@ This will register the Todo component class with the default alias name **demoTo When you add a component to a page or layout you can configure it using properties. The properties are defined with the `defineProperties` method of the component class. The next example shows how to define a component property: - public function defineProperties() - { - return [ - 'maxItems' => [ - 'title' => 'Max items', - 'description' => 'The most amount of todo items allowed', - 'default' => 10, - 'type' => 'string', - 'validationPattern' => '^[0-9]+$', - 'validationMessage' => 'The Max Items property can contain only numeric symbols' - ] - ]; - } +```php +public function defineProperties() +{ + return [ + 'maxItems' => [ + 'title' => 'Max items', + 'description' => 'The most amount of todo items allowed', + 'default' => 10, + 'type' => 'string', + 'validationPattern' => '^[0-9]+$', + 'validationMessage' => 'The Max Items property can contain only numeric symbols' + ] + ]; +} +``` The method should return an array with the property keys as indexes and property parameters as values. The property keys are used for accessing the component property values inside the component class. The property parameters are defined with an array with the following keys: + +
    + Key | Description ------------- | ------------- -**title** | required, the property title, it is used by the component Inspector in the CMS backend. -**description** | required, the property description, it is used by the component Inspector in the CMS backend. -**default** | optional, the default property value to use when the component is added to a page or layout in the CMS backend. -**type** | optional, specifies the property type. The type defines the way how the property is displayed in the Inspector. Currently supported types are **string**, **checkbox**, **dropdown** and **set**. Default value: **string**. -**validationPattern** | optional Regular Expression to use when a user enters the property value in the Inspector. The validation can be used only with **string** properties. -**validationMessage** | optional error message to display if the validation fails. -**required** | optional, forces field to be filled. Uses validationMessage when left empty. -**placeholder** | optional placeholder for string and dropdown properties. -**options** | optional array of options for dropdown properties. -**depends** | an array of property names a dropdown property depends on. See the [dropdown properties](#dropdown-properties) below. -**group** | an optional group name. Groups create sections in the Inspector simplifying the user experience. Use a same group name in multiple properties to combine them. -**showExternalParam** | specifies visibility of the External Parameter editor for the property in the Inspector. Default value: **true**. +`title` | required, the property title, it is used by the component Inspector in the CMS backend. +`description` | required, the property description, it is used by the component Inspector in the CMS backend. +`default` | optional, the default property value to use when the component is added to a page or layout in the CMS backend. +`type` | optional, specifies the property type. The type defines the way how the property is displayed in the Inspector. Currently supported types are `string`, `checkbox`, `dropdown` and `set`. Default value: `string`. +`validationPattern` | optional Regular Expression to use when a user enters the property value in the Inspector. The validation can be used only with `string` properties. +`validationMessage` | optional error message to display if the validation fails. +`required` | optional, forces field to be filled. Uses validationMessage when left empty. +`placeholder` | optional placeholder for string and dropdown properties. +`options` | optional array of options for dropdown properties. +`depends` | an array of property names a dropdown property depends on. See the [dropdown properties](#dropdown-properties) below. +`group` | an optional group name. Groups create sections in the Inspector simplifying the user experience. Use a same group name in multiple properties to combine them. +`showExternalParam` | specifies visibility of the External Parameter editor for the property in the Inspector. Default value: `true`. Inside the component you can read the property value with the `property` method: - $this->property('maxItems'); +```php +$this->property('maxItems'); +``` If the property value is not defined, you can supply the default value as a second parameter of the `property` method: - $this->property('maxItems', 6); +```php +$this->property('maxItems', 6); +``` You can also load all the properties as array: - $properties = $this->getProperties(); +```php +$properties = $this->getProperties(); +``` To access the property from the Twig partials for the component, utilize the `__SELF__` variable which refers to the Component object: - `{{ __SELF__.property('maxItems') }}` +```twig +{{ __SELF__.property('maxItems') }} +``` ### Dropdown and Set properties -The option list for dropdown and set properties can be static or dynamic. Static options are defined with the `options` element of the property definition. Example: +A `dropdown` allows you to select a single value from a series of options. A `set` allows you to select multiple values from a series of options. - public function defineProperties() - { - return [ - 'units' => [ - 'title' => 'Units', - 'type' => 'dropdown', - 'default' => 'imperial', - 'placeholder' => 'Select units', - 'options' => ['metric'=>'Metric', 'imperial'=>'Imperial'] - ] - ]; - } +The option list for `dropdown` and `set` properties can be static or dynamic. Static options are defined with the `options` property for dropdowns and the `items` property for sets. Example: -The list of options could be fetched dynamically from the server when the Inspector is displayed. If the `options` parameter is omitted in a dropdown or set property definition the option list is considered dynamic. The component class must define a method returning the option list. The method should have a name in the following format: `get*Property*Options`, where **Property** is the property name, for example: `getCountryOptions`. The method returns an array of options with the option values as keys and option labels as values. Example of a dynamic dropdown list definition: +```php +public function defineProperties() +{ + return [ + 'units' => [ + 'title' => 'Units', + 'type' => 'dropdown', + 'default' => 'imperial', + 'placeholder' => 'Select units', + 'options' => ['metric'=>'Metric', 'imperial'=>'Imperial'] + ], + 'favoriteFruits' => [ + 'title' => 'Favorite fruits', + 'type' => 'set', + 'items' => [ + 'apples' => 'Apples', + 'bananas' => 'Bananas', + 'cantaloupes' => 'Cantaloupes', + ], + 'default' => ['bananas', 'cantaloupes'], + ], + ]; +} +``` - public function defineProperties() - { - return [ - 'country' => [ - 'title' => 'Country', - 'type' => 'dropdown', - 'default' => 'us' - ] - ]; - } +The list of options or items could be fetched dynamically from the server when the Inspector is displayed. If the `options` parameter is omitted for dropdowns or the `items` parameter is omitted for sets, the list is considered dynamic. The component class must define a method returning this list. The method should have a name in the following format: `get*Property*Options`, where **Property** is the property name, for example: `getCountryOptions`. The method returns an array of options with the option values as keys and option labels as values. Example of a dynamic dropdown list definition: - public function getCountryOptions() - { - return ['us'=>'United states', 'ca'=>'Canada']; - } +```php +public function defineProperties() +{ + return [ + 'country' => [ + 'title' => 'Country', + 'type' => 'dropdown', + 'default' => 'us' + ] + ]; +} -Dynamic dropdown and set lists can depend on other properties. For example, the state list could depend on the selected country. The dependencies are declared with the `depends` parameter in the property definition. The next example defines two dynamic dropdown properties and the state list depends on the country: +public function getCountryOptions() +{ + return ['us' => 'United states', 'ca' => 'Canada']; +} +``` - public function defineProperties() - { - return [ - 'country' => [ - 'title' => 'Country', - 'type' => 'dropdown', - 'default' => 'us' - ], - 'state' => [ - 'title' => 'State', - 'type' => 'dropdown', - 'default' => 'dc', - 'depends' => ['country'], - 'placeholder' => 'Select a state' - ] - ]; - } +Dynamic `dropdown` and `set` lists can depend on other properties. For example, the state list could depend on the selected country. The dependencies are declared with the `depends` parameter in the property definition. The next example defines two dynamic dropdown properties and the state list depends on the country: + +```php +public function defineProperties() +{ + return [ + 'country' => [ + 'title' => 'Country', + 'type' => 'dropdown', + 'default' => 'us' + ], + 'state' => [ + 'title' => 'State', + 'type' => 'dropdown', + 'default' => 'dc', + 'depends' => ['country'], + 'placeholder' => 'Select a state' + ] + ]; +} +``` In order to load the state list you should know what country is currently selected in the Inspector. The Inspector POSTs all property values to the `getPropertyOptions` handler, so you can do the following: - public function getStateOptions() - { - $countryCode = Request::input('country'); // Load the country property value from POST +```php +public function getStateOptions() +{ + $countryCode = Request::input('country'); // Load the country property value from POST - $states = [ - 'ca' => ['ab'=>'Alberta', 'bc'=>'British columbia'], - 'us' => ['al'=>'Alabama', 'ak'=>'Alaska'] - ]; + $states = [ + 'ca' => ['ab' => 'Alberta', 'bc' => 'British Columbia'], + 'us' => ['al' => 'Alabama', 'ak' => 'Alaska'], + ]; - return $states[$countryCode]; - } + return $states[$countryCode]; +} +``` ### Page list properties Sometimes components need to create links to the website pages. For example, the blog post list contains links to the blog post details page. In this case the component should know the post details page file name (then it can use the [page Twig filter](../markup/filter-page)). Winter includes a helper for creating dynamic dropdown page lists. The next example defines the postPage property which displays a list of pages: - public function defineProperties() - { - return [ - 'postPage' => [ - 'title' => 'Post page', - 'type' => 'dropdown', - 'default' => 'blog/post' - ] - ]; - } +```php +public function defineProperties() +{ + return [ + 'postPage' => [ + 'title' => 'Post page', + 'type' => 'dropdown', + 'default' => 'blog/post' + ] + ]; +} - public function getPostPageOptions() - { - return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); - } +public function getPostPageOptions() +{ + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); +} +``` ## Routing parameters Components can directly access routing parameter values defined in the [URL of the page](../cms/pages#url-syntax). - // Returns the URL segment value, eg: /page/:post_id - $postId = $this->param('post_id'); +```php +// Returns the URL segment value, eg: /page/:post_id +$postId = $this->param('post_id'); +``` In some cases a [component property](#component-properties) may act as a hard coded value or reference the value from the URL. This hard coded example shows the blog post with an identifier `2` being used: - url = "/blog/hard-coded-page" +```ini +url = "/blog/hard-coded-page" - [blogPost] - id = "2" +[blogPost] +id = "2" +``` Alternatively the value can be referenced dynamically from the page URL using an [external property value](../cms/components#external-property-values): - url = "/blog/:my_custom_parameter" +```ini +url = "/blog/:my_custom_parameter" - [blogPost] - id = "{{ :my_custom_parameter }}" +[blogPost] +id = "{{ :my_custom_parameter }}" +``` In both cases the value can be retrieved by using the `property` method: - $this->property('id'); +```php +$this->property('id'); +``` If you need to access the routing parameter name: - $this->paramName('id'); // Returns "my_custom_parameter" +```php +$this->paramName('id'); // Returns "my_custom_parameter" +``` ## Handling the page execution cycle Components can be involved in the Page execution cycle events by overriding the `onRun` method in the component class. The CMS controller executes this method every time when the page or layout loads. Inside the method you can inject variables to the Twig environment through the `page` property: - public function onRun() - { - // This code will be executed when the page or layout is - // loaded and the component is attached to it. +```php +public function onRun() +{ + // This code will be executed when the page or layout is + // loaded and the component is attached to it. - $this->page['var'] = 'value'; // Inject some variable to the page - } + $this->page['var'] = 'value'; // Inject some variable to the page +} +``` ### Page execution life cycle handlers @@ -296,44 +358,52 @@ When a page loads, Winter executes handler functions that could be defined in th Sometimes you may wish to execute code at the time the component class is first instantiated. You may override the `init` method in the component class to handle any initialization logic, this will execute before AJAX handlers and before the page execution life cycle. For example, this method can be used for attaching another component to the page dynamically. - public function init() - { - $this->addComponent('Acme\Blog\Components\BlogPosts', 'blogPosts'); - } +```php +public function init() +{ + $this->addComponent('Acme\Blog\Components\BlogPosts', 'blogPosts'); +} +``` ### Halting with a response Like all methods in the [page execution life cycle](../cms/layouts#layout-life-cycle), if the `onRun` method in a component returns a value, this will stop the cycle at this point and return the response to the browser. Here we return an access denied message using the `Response` facade: - public function onRun() - { - if (true) { - return Response::make('Access denied!', 403); - } +```php +public function onRun() +{ + if (true) { + return Response::make('Access denied!', 403); } +} +``` You can also return a 404 response from the `onRun` method: - public function onRun() - { - if (true) { - $this->setStatusCode(404); - return $this->controller->run('404'); - } +```php +public function onRun() +{ + if (true) { + $this->setStatusCode(404); + return $this->controller->run('404'); } +} +``` ## AJAX handlers Components can host AJAX event handlers. They are defined in the component class exactly like they can be defined in the [page or layout code](../ajax/handlers). An example AJAX handler method defined in a component class: - public function onAddItem() - { - $value1 = post('value1'); - $value2 = post('value2'); - $this->page['result'] = $value1 + $value2; - } +```php +public function onAddItem() +{ + $value1 = post('value1'); + $value2 = post('value2'); + $this->page['result'] = $value1 + $value2; +} +``` If the alias for this component was *demoTodo* this handler can be accessed by `demoTodo::onAddItem`. Please see the [Calling AJAX handlers defined in components](../ajax/handlers#calling-handlers) article for details about using AJAX with components. @@ -344,100 +414,128 @@ All components can come with default markup that is used when including it on a The default component markup should be placed in a file named **default.htm**. For example, the default markup for the Demo ToDo component is defined in the file **/plugins/winter/demo/components/todo/default.htm**. It can then be inserted anywhere on the page by using the `{% component %}` tag: - url = "/todo" +```twig +url = "/todo" - [demoTodo] - == - {% component 'demoTodo' %} +[demoTodo] +== +{% component 'demoTodo' %} +``` The default markup can also take parameters that override the [component properties](#component-properties) at the time they are rendered. - {% component 'demoTodo' maxItems="7" %} +```twig +{% component 'demoTodo' maxItems="7" %} +``` These properties will not be available in the `onRun` method since they are established after the page cycle has completed. Instead they can be processed by overriding the `onRender` method in the component class. The CMS controller executes this method before the default markup is rendered. - public function onRender() - { - // This code will be executed before the default component - // markup is rendered on the page or layout. +```php +public function onRender() +{ + // This code will be executed before the default component + // markup is rendered on the page or layout. - $this->page['var'] = 'Maximum items allowed: ' . $this->property('maxItems'); - } + $this->page['var'] = 'Maximum items allowed: ' . $this->property('maxItems'); +} +``` ## Component partials In addition to the default markup, components can also offer additional partials that can be used on the frontend or within the default markup itself. If the Demo ToDo component had a **pagination** partial, it would be located in **/plugins/winter/demo/components/todo/pagination.htm** and displayed on the page using: - {% partial 'demoTodo::pagination' %} +```twig +{% partial 'demoTodo::pagination' %} +``` A relaxed method can be used that is contextual. If called inside a component partial, it will directly refer to itself. If called inside a theme partial, it will scan all components used on the page/layout for a matching partial name and use that. - {% partial '@pagination' %} +```twig +{% partial '@pagination' %} +``` Multiple components can share partials by placing the partial file in a directory called **components/partials**. The partials found in this directory are used as a fallback when the usual component partial cannot be found. For example, a shared partial located in **/plugins/acme/blog/components/partials/shared.htm** can be displayed on the page by any component using: - {% partial '@shared' %} +```twig +{% partial '@shared' %} +``` ### Referencing "self" Components can reference themselves inside their partials by using the `__SELF__` variable. By default it will return the component's short name or [alias](../cms/components#aliases). -
    - [...] -
    +```twig +
    + [...] +
    +``` Components can also reference their own properties. - {% for item in __SELF__.items() %} - {{ item }} - {% endfor %} +```twig +{% for item in __SELF__.items() %} + {{ item }} +{% endfor %} +``` If inside a component partial you need to render another component partial concatenate the `__SELF__` variable with the partial name: - {% partial __SELF__~"::screenshot-list" %} +```twig +{% partial __SELF__~"::screenshot-list" %} +``` ### Unique identifier If an identical component is called twice on the same page, an `id` property can be used to reference each instance. - {{__SELF__.id}} +```twig +{{ __SELF__.id }} +``` The ID is unique each time the component is displayed. - - {% component 'demoTodo' %} +```twig + +{% component 'demoTodo' %} - - {% component 'demoTodo' %} + +{% component 'demoTodo' %} +``` ## Rendering partials from code You may programmatically render component partials inside the PHP code using the `renderPartial` method. This will check the component for the partial named `component-partial.htm` and return the result as a string. The second parameter is used for passing view variables. The same [path resolution logic](#component-partials) applies when you render a component partial in PHP as it does with Twig; use the `@` prefix to refer to partials within the component itself. - $content = $this->renderPartial('@component-partial.htm'); +```php +$content = $this->renderPartial('@component-partial.htm'); - $content = $this->renderPartial('@component-partial.htm', [ - 'name' => 'John Smith' - ]); +$content = $this->renderPartial('@component-partial.htm', [ + 'name' => 'John Smith' +]); +``` For example, to render a partial as a response to an [AJAX handler](../ajax/handlers): - function onGetTemplate() - { - return ['#someDiv' => $this->renderPartial('@component-partial.htm')]; - } +```php +function onGetTemplate() +{ + return ['#someDiv' => $this->renderPartial('@component-partial.htm')]; +} +``` Another example could be overriding the entire page view response by returning a value from the `onRun` [page cycle method](#page-cycle). This code will specifically return an XML response using the `Response` facade: - public function onRun() - { - $content = $this->renderPartial('@default.htm'); - return Response::make($content)->header('Content-Type', 'text/xml'); - } +```php +public function onRun() +{ + $content = $this->renderPartial('@default.htm'); + return Response::make($content)->header('Content-Type', 'text/xml'); +} +``` ## Injecting page assets with components diff --git a/plugin-extending.md b/plugin-extending.md index 379472c0..4a2f328a 100644 --- a/plugin-extending.md +++ b/plugin-extending.md @@ -25,20 +25,24 @@ The [Event service](../services/events) is the primary way to inject or modify t The most common place to subscribe to an event is the `boot` method of a [Plugin registration file](registration#registration-methods). For example, when a user is first registered you might want to add them to a third party mailing list, this could be achieved by subscribing to a `winter.user.register` global event. - public function boot() - { - Event::listen('winter.user.register', function ($user) { - // Code to register $user->email to mailing list - }); - } +```php +public function boot() +{ + Event::listen('winter.user.register', function ($user) { + // Code to register $user->email to mailing list + }); +} +``` The same can be achieved by extending the model's constructor and using a local event. - User::extend(function ($model) { - $model->bindEvent('user.register', function () use ($model) { - // Code to register $model->email to mailing list - }); +```php +User::extend(function ($model) { + $model->bindEvent('user.register', function () use ($model) { + // Code to register $model->email to mailing list }); +}); +``` ### Declaring / Firing events @@ -47,28 +51,36 @@ You can fire events globally (through the Event service) or locally. Local events are fired by calling `fireEvent()` on an instance of an object that implements `Winter\Storm\Support\Traits\Emitter`. Since local events are only fired on a specific object instance, it is not required to namespace them as it is less likely that a given project would have multiple events with the same name being fired on the same objects within a local context. - $this->fireEvent('post.beforePost', [$firstParam, $secondParam]); +```php +$this->fireEvent('post.beforePost', [$firstParam, $secondParam]); +``` Global events are fired by calling `Event::fire()`. As these events are global across the entire application, it is best practice to namespace them by including the vendor information in the name of the event. If your plugin Author is ACME and the plugin name is Blog, then any global events provided by the ACME.Blog plugin should be prefixed with `acme.blog`. - Event::fire('acme.blog.post.beforePost', [$firstParam, $secondParam]); +```php +Event::fire('acme.blog.post.beforePost', [$firstParam, $secondParam]); +``` If both global & local events are provided at the same place it's best practice to fire the local event before the global event so that the local event takes priority. Additionally, the global event should provide the object instance that the local event was fired on as the first parameter. - $this->fireEvent('post.beforePost', [$firstParam, $secondParam]); - Event::fire('winter.blog.beforePost', [$this, $firstParam, $secondParam]); +```php +$this->fireEvent('post.beforePost', [$firstParam, $secondParam]); +Event::fire('winter.blog.beforePost', [$this, $firstParam, $secondParam]); +``` Once this event has been subscribed to, the parameters are available in the handler method. For example: - // Global - Event::listen('acme.blog.post.beforePost', function ($post, $param1, $param2) { - Log::info($post->name . 'posted. Parameters: ' . $param1 . ' ' . $param2); - }); +```php +// Global +Event::listen('acme.blog.post.beforePost', function ($post, $param1, $param2) { + Log::info($post->name . 'posted. Parameters: ' . $param1 . ' ' . $param2); +}); - // Local - $post->bindEvent('post.beforePost', function ($param1, $param2) use ($post) { - Log::info($post->name . 'posted. Parameters: ' . $param1 . ' ' . $param2); - }); +// Local +$post->bindEvent('post.beforePost', function ($param1, $param2) use ($post) { + Log::info($post->name . 'posted. Parameters: ' . $param1 . ' ' . $param2); +}); +``` ## Extending backend views @@ -77,23 +89,29 @@ Sometimes you may wish to allow a backend view file or partial to be extended, s Place this code in your view file: - +```php + +``` This will allow other plugins to inject HTML to this area by hooking the event and returning the desired markup. - Event::listen('backend.auth.extendSigninView', function ($controller, $firstParam) { - return 'Sign in with Google!'; - }); +```php +Event::listen('backend.auth.extendSigninView', function ($controller, $firstParam) { + return 'Sign in with Google!'; +}); +``` > **NOTE**: The first parameter in the event handler will always be the calling object (the controller). The above example would output the following markup: - +```html + +``` ## Usage examples @@ -105,37 +123,39 @@ These are some practical examples of how events can be used. This example will modify the [`model.getAttribute`](../events/event/model.beforeGetAttribute) event of the `User` model by binding to its local event. This is carried out inside the `boot` method of the [Plugin registration file](registration#routing-initialization). In both cases, when the `$model->foo` attribute is accessed it will return the value *bar*. - class Plugin extends PluginBase +```php +class Plugin extends PluginBase +{ + [...] + + public function boot() { - [...] + // Local event hook that affects all users + User::extend(function ($model) { + $model->bindEvent('model.getAttribute', function ($attribute, $value) { + if ($attribute === 'foo') { + return 'bar'; + } + }); + }); + + // Double event hook that affects user #2 only + User::extend(function ($model) { + $model->bindEvent('model.afterFetch', function () use ($model) { + if ($model->id !== 2) { + return; + } - public function boot() - { - // Local event hook that affects all users - User::extend(function ($model) { $model->bindEvent('model.getAttribute', function ($attribute, $value) { if ($attribute === 'foo') { return 'bar'; } }); }); - - // Double event hook that affects user #2 only - User::extend(function ($model) { - $model->bindEvent('model.afterFetch', function () use ($model) { - if ($model->id !== 2) { - return; - } - - $model->bindEvent('model.getAttribute', function ($attribute, $value) { - if ($attribute === 'foo') { - return 'bar'; - } - }); - }); - }); - } + }); } +} +``` ### Extending backend forms @@ -144,44 +164,46 @@ There are a number of ways to [extend backend forms](../backend/forms#extend-for This example will listen to the [`backend.form.extendFields`](../events/event/backend.form.extendFields) global event of the `Backend\Widget\Form` widget and inject some extra fields when the Form widget is being used to modify a user. This event is also subscribed inside the `boot` method of the [Plugin registration file](registration#routing-initialization). - class Plugin extends PluginBase - { - [...] - - public function boot() - { - // Extend all backend form usage - Event::listen('backend.form.extendFields', function($widget) { - // Only apply this listener when the Users controller is being used - if (!$widget->getController() instanceof \Winter\User\Controllers\Users) { - return; - } - - // Only apply this listener when the User model is being modified - if (!$widget->model instanceof \Winter\User\Models\User) { - return; - } +```php +class Plugin extends PluginBase +{ + [...] - // Only apply this listener when the Form widget in question is a root-level - // Form widget (not a repeater, nestedform, etc) - if ($widget->isNested) { - return; - } - - // Add an extra birthday field - $widget->addFields([ - 'birthday' => [ - 'label' => 'Birthday', - 'comment' => 'Select the users birthday', - 'type' => 'datepicker' - ] - ]); - - // Remove a Surname field - $widget->removeField('surname'); - }); - } + public function boot() + { + // Extend all backend form usage + Event::listen('backend.form.extendFields', function($widget) { + // Only apply this listener when the Users controller is being used + if (!$widget->getController() instanceof \Winter\User\Controllers\Users) { + return; + } + + // Only apply this listener when the User model is being modified + if (!$widget->model instanceof \Winter\User\Models\User) { + return; + } + + // Only apply this listener when the Form widget in question is a root-level + // Form widget (not a repeater, nestedform, etc) + if ($widget->isNested) { + return; + } + + // Add an extra birthday field + $widget->addFields([ + 'birthday' => [ + 'label' => 'Birthday', + 'comment' => 'Select the users birthday', + 'type' => 'datepicker' + ] + ]); + + // Remove a Surname field + $widget->removeField('surname'); + }); } +} +``` > **NOTE:** In some cases (adding fields that should be made translatable by [Winter.Translate](https://github.com/wintercms/wn-translate-plugin) for example), you may want to extend the [`backend.form.extendFieldsBefore`](../events/event/backend.form.extendFieldsBefore) event instead. @@ -190,106 +212,116 @@ This example will listen to the [`backend.form.extendFields`](../events/event/ba This example will modify the [`backend.list.extendColumns`](../events/event/backend.list.extendColumns) global event of the `Backend\Widget\Lists` class and inject some extra columns values under the conditions that the list is being used to modify a user. This event is also subscribed inside the `boot` method of the [Plugin registration file](registration#routing-initialization). - class Plugin extends PluginBase - { - [...] - - public function boot() - { - // Extend all backend list usage - Event::listen('backend.list.extendColumns', function ($widget) { - // Only for the User controller - if (!$widget->getController() instanceof \Winter\User\Controllers\Users) { - return; - } - - // Only for the User model - if (!$widget->model instanceof \Winter\User\Models\User) { - return; - } +```php +class Plugin extends PluginBase +{ + [...] - // Add an extra birthday column - $widget->addColumns([ - 'birthday' => [ - 'label' => 'Birthday' - ], - ]); - - // Remove a Surname column - $widget->removeColumn('surname'); - }); - } + public function boot() + { + // Extend all backend list usage + Event::listen('backend.list.extendColumns', function ($widget) { + // Only for the User controller + if (!$widget->getController() instanceof \Winter\User\Controllers\Users) { + return; + } + + // Only for the User model + if (!$widget->model instanceof \Winter\User\Models\User) { + return; + } + + // Add an extra birthday column + $widget->addColumns([ + 'birthday' => [ + 'label' => 'Birthday' + ], + ]); + + // Remove a Surname column + $widget->removeColumn('surname'); + }); } +} +``` ### Extending a component This example will declare a new global event `winter.forum.topic.post` and local event called `topic.post` inside a `Topic` component. This is carried out in the [Component class definition](components#component-class-definition). - class Topic extends ComponentBase +```php +class Topic extends ComponentBase +{ + public function onPost() { - public function onPost() - { - [...] - - /* - * Extensibility - */ - $this->fireEvent('topic.post', [$post, $postUrl]); - Event::fire('winter.forum.topic.post', [$this, $post, $postUrl]); - } + [...] + + /* + * Extensibility + */ + $this->fireEvent('topic.post', [$post, $postUrl]); + Event::fire('winter.forum.topic.post', [$this, $post, $postUrl]); } +} +``` Next this will demonstrate how to hook to this new event from inside the [page execution life cycle](../cms/layouts#dynamic-pages). This will write to the trace log when the `onPost` event handler is called inside the `Topic` component (above). - [topic] - slug = "{{ :slug }}" - == - function onInit() - { - $this['topic']->bindEvent('topic.post', function($post, $postUrl) { - trace_log('A post has been submitted at '.$postUrl); - }); - } +```php +[topic] +slug = "{{ :slug }}" +== +function onInit() +{ + $this['topic']->bindEvent('topic.post', function($post, $postUrl) { + trace_log('A post has been submitted at '.$postUrl); + }); +} +``` ### Extending the backend menu This example will replace the label for CMS and Pages in the backend with *...*. - class Plugin extends PluginBase - { - [...] +```php +class Plugin extends PluginBase +{ + [...] - public function boot() - { - Event::listen('backend.menu.extendItems', function($manager) { + public function boot() + { + Event::listen('backend.menu.extendItems', function($manager) { - $manager->addMainMenuItems('Winter.Cms', [ - 'cms' => [ - 'label' => '...' - ] - ]); + $manager->addMainMenuItems('Winter.Cms', [ + 'cms' => [ + 'label' => '...' + ] + ]); - $manager->addSideMenuItems('Winter.Cms', 'cms', [ - 'pages' => [ - 'label' => '...' - ] - ]); + $manager->addSideMenuItems('Winter.Cms', 'cms', [ + 'pages' => [ + 'label' => '...' + ] + ]); - }); - } + }); } +} +``` Similarly we can remove the menu items with the same event: - Event::listen('backend.menu.extendItems', function($manager) { +```php +Event::listen('backend.menu.extendItems', function($manager) { - $manager->removeMainMenuItem('Winter.Cms', 'cms'); - $manager->removeSideMenuItem('Winter.Cms', 'cms', 'pages'); + $manager->removeMainMenuItem('Winter.Cms', 'cms'); + $manager->removeSideMenuItem('Winter.Cms', 'cms', 'pages'); - $manager->removeSideMenuItems('Winter.Cms', 'cms', [ - 'pages', - 'partials' - ]); - }); + $manager->removeSideMenuItems('Winter.Cms', 'cms', [ + 'pages', + 'partials' + ]); +}); +``` diff --git a/plugin-localization.md b/plugin-localization.md index de5c2c35..da132dd4 100644 --- a/plugin-localization.md +++ b/plugin-localization.md @@ -13,60 +13,71 @@ Plugins can have localization files in the **lang** subdirectory of the plugin d Below is an example of the plugin's lang directory: - plugins/ - acme/ - todo/ <=== Plugin directory - lang/ <=== Localization directory - en/ <=== Language directory - lang.php <=== Localization file - fr/ - lang.php - +```css +πŸ“‚ plugins + β”— πŸ“‚ acme + β”— πŸ“‚ todo <=== Plugin directory + β”— πŸ“‚ lang <=== Localization directory + ┣ πŸ“‚ en <=== Language directory + ┃ β”— πŸ“œ lang.php <=== Localization file + β”— πŸ“‚ fr + β”— πŸ“œ lang.php +``` The **lang.php** file should define and return an array of any depth, for example: - [ - 'name' => 'Winter CMS', - 'tagline' => 'Getting back to basics' - ] - ]; +return [ + 'app' => [ + 'name' => 'Winter CMS', + 'tagline' => 'Getting back to basics' + ] +]; +``` The **validation.php** file has a similar structure to the **lang.php** and is used to specify your [custom validation](../services/validation#localization) messages in a language file, for example: - 'We need to know your xxx!', - 'email.required' => 'We need to know your e-mail address!', - ]; +return [ + 'required' => 'We need to know your xxx!', + 'email.required' => 'We need to know your e-mail address!', +]; +``` ## Accessing localization strings The localization strings can be loaded with the `Lang` class. The parameter it accepts is the localization key string that consists of the plugin name, the localization file name and the path to the localization string inside the array returned from the file. The next example loads the **app.name** string from the plugins/acme/blog/lang/en/lang.php file (the language is set with the `locale` parameter in the `config/app.php` configuration file): - echo Lang::get('acme.blog::lang.app.name'); +```php +echo Lang::get('acme.blog::lang.app.name'); +``` ## Overriding localization strings System users can override plugin localization strings without altering the plugins' files. This is done by adding localization files to the **lang** directory. For example, to override the lang.php file of the **acme/blog** plugin you should create the file in the following location: - lang/ <=== App localization directory - en/ <=== Language directory - acme/ <=== Plugin / Module directory - blog/ <===^ - lang.php <=== Localization override file +```css +πŸ“‚ lang <=== App localization directory + β”— πŸ“‚ en <=== Language directory + β”— πŸ“‚ acme <=== Plugin / Module directory + β”— πŸ“‚ blog <===^ + β”— πŸ“œ lang.php <=== Localization override file +``` The file could contain only strings you want to override, there is no need to replace the entire file. Example: - [ - 'name' => 'Winter CMS!' - ] - ]; +return [ + 'app' => [ + 'name' => 'Winter CMS!' + ] +]; +``` diff --git a/plugin-registration.md b/plugin-registration.md index c5fbbba7..700deb61 100644 --- a/plugin-registration.md +++ b/plugin-registration.md @@ -45,9 +45,9 @@ The simplest plugins only require the **Plugin.php** file described below. ```css πŸ“‚ plugins - ┣ πŸ“‚ myauthor /* Author name */ - ┃ ┣ πŸ“‚ myplugin /* Plugin name */ - ┃ ┃ β”— πŸ“œ Plugin.php /* Plugin registration file, required */ + β”— πŸ“‚ myauthor /* Author name */ + β”— πŸ“‚ myplugin /* Plugin name */ + β”— πŸ“œ Plugin.php /* Plugin registration file, required */ ``` @@ -59,20 +59,21 @@ The following is an example of what most plugins would end up looking like when ```css πŸ“‚ plugins - ┣ πŸ“‚ myauthor /* Author name */ - ┃ ┣ πŸ“‚ myplugin /* Plugin name */ - ┃ ┃ ┣ πŸ“‚ assets /* CSS, JavaScript and image assets for pages and components */ - ┃ ┃ ┣ πŸ“‚ controllers /* Backend controllers */ - ┃ ┃ ┣ πŸ“‚ lang /* Localization files */ - ┃ ┃ ┃ β”— πŸ“‚ en /* Specific locale folder */ - ┃ ┃ ┃ ┃ β”— πŸ“œ lang.php /* Translations */ - ┃ ┃ ┣ πŸ“‚ models /* Models */ - ┃ ┃ ┣ πŸ“‚ updates /* Database migrations */ - ┃ ┃ ┃ β”— πŸ“œ version.yaml /* Changelog */ - ┃ ┃ ┣ πŸ“‚ views /* Custom view files */ - ┃ ┃ ┃ β”— πŸ“‚ mail /* Custom mail templates */ - ┃ ┃ ┣ πŸ“œ README.md /* Documentation describing the purpose of the plugin */ - ┃ ┃ β”— πŸ“œ Plugin.php /* Plugin registration class */ + β”— πŸ“‚ myauthor /* Author name */ + β”— πŸ“‚ myplugin /* Plugin name */ + ┣ πŸ“‚ assets /* CSS, JavaScript and image assets for pages and components */ + ┣ πŸ“‚ components /* Frontend components */ + ┣ πŸ“‚ controllers /* Backend controllers */ + ┣ πŸ“‚ lang /* Localization files */ + ┃ β”— πŸ“‚ en /* Specific locale folder */ + ┃ β”— πŸ“œ lang.php /* Translations */ + ┣ πŸ“‚ models /* Models */ + ┣ πŸ“‚ updates /* Database migrations */ + ┃ β”— πŸ“œ version.yaml /* Changelog */ + ┣ πŸ“‚ views /* Custom view files */ + ┃ β”— πŸ“‚ mail /* Custom mail templates */ + ┣ πŸ“œ README.md /* Documentation describing the purpose of the plugin */ + β”— πŸ“œ Plugin.php /* Plugin registration class */ ``` @@ -82,62 +83,67 @@ The following is an example of what a complex plugin could look like when using ```css πŸ“‚ plugins - ┣ πŸ“‚ myauthor /* Author name */ - ┃ ┣ πŸ“‚ myplugin /* Plugin name */ - ┃ ┃ ┣ πŸ“‚ assets /* CSS, JavaScript and image assets for pages and components */ - ┃ ┃ ┃ ┣ πŸ“‚ css - ┃ ┃ ┃ ┣ πŸ“‚ favicons - ┃ ┃ ┃ ┣ πŸ“‚ images - ┃ ┃ ┃ ┣ πŸ“‚ js - ┃ ┃ ┃ β”— πŸ“‚ scss - ┃ ┃ ┣ πŸ“‚ behaviors /* Any custom behaviors provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ classes /* Any custom classes provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ config /* Configuration files */ - ┃ ┃ ┃ β”— πŸ“œ config.php - ┃ ┃ ┣ πŸ“‚ console /* Any custom CLI commands provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ controllers /* Backend controllers */ - ┃ ┃ ┃ ┣ πŸ“‚ records /* Directory for the view and configuration files for the given controller */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ _list_toolbar.htm /* List toolbar partial file */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ config_filter.yaml /* Configuration for the Filter widget present on the controller lists */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ config_form.yaml /* Configuration for the Form widget present on the controller */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ config_importexport.yaml /* Configuration for the Import/Export behavior */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ config_list.yaml /* Configuration for the Lists widget present on the controller */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ config_relation.yaml /* Configuration for the RelationController behavior */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ create.htm /* View file for the create action */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ index.htm /* View file for the index action */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ preview.htm /* View file for the preview action */ - ┃ ┃ ┃ ┃ β”— πŸ“œ update.htm /* View file for the update action */ - ┃ ┃ ┃ ┣ πŸ“œ Records.php /* Backend controller for the Record model */ - ┃ ┃ ┣ πŸ“‚ docs /* Any plugin-specific documentation should live here */ - ┃ ┃ ┣ πŸ“‚ formwidgets /* Any custom FormWidgets provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ lang /* Localization files */ - ┃ ┃ ┃ β”— πŸ“‚ en /* Specific locale folder */ - ┃ ┃ ┃ ┃ β”— πŸ“œ lang.php /* Translations for that locale */ - ┃ ┃ ┣ πŸ“‚ layouts /* Any custom backend layouts used by the plugin */ - ┃ ┃ ┣ πŸ“‚ models /* Models provided by the plugin */ - ┃ ┃ ┃ ┣ πŸ“‚ record /* Directory containing configuration files specific to that model */ - ┃ ┃ ┃ ┃ ┣ πŸ“œ columns.yaml /* Configuration file used for the Lists widget */ - ┃ ┃ ┃ ┃ β”— πŸ“œ fields.yaml /* Configuration file used for the Form widget */ - ┃ ┃ ┃ ┣ πŸ“œ Record.php /* Model class for the Record model */ - ┃ ┃ ┣ πŸ“‚ partials /* Any custom partials used by the plugin */ - ┃ ┃ ┣ πŸ“‚ reportwidgets /* Any custom ReportWidgets provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ tests /* Test suite for the plugin */ - ┃ ┃ ┣ πŸ“‚ traits /* Any custom Traits provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ updates /* Database migrations */ - ┃ ┃ ┃ ┃ ┣ πŸ“‚ v1.0.0 /* Migrations for a specific version of the plugin */ - ┃ ┃ ┃ ┃ ┃ β”— πŸ“œ create_records_table.php /* Database migration file, referenced in version.yaml */ - ┃ ┃ ┃ β”— πŸ“œ version.yaml /* Changelog */ - ┃ ┃ ┣ πŸ“‚ views /* Custom view files */ - ┃ ┃ ┃ β”— πŸ“‚ mail /* Custom mail templates provided by the plugin */ - ┃ ┃ ┣ πŸ“‚ widgets /* Any custom Widgets provided by the plugin */ - ┃ ┃ ┣ πŸ“œ LICENSE /* License file */ - ┃ ┃ ┣ πŸ“œ README.md /* Documentation describing the purpose of the plugin */ - ┃ ┃ ┣ πŸ“œ Plugin.php /* Plugin registration file */ - ┃ ┃ ┣ πŸ“œ composer.json /* Composer file to manage dependencies for the plugin */ - ┃ ┃ ┣ πŸ“œ helpers.php /* Global helpers provided by the plugin loaded via composer.json */ - ┃ ┃ ┣ πŸ“œ phpunit.xml /* Unit testing configuration */ - ┃ ┃ ┣ πŸ“œ plugin.yaml /* Simplified plugin registration configuration YAML file, used by Builder plugin */ - ┃ ┃ β”— πŸ“œ routes.php /* Any custom routes provided by the plugin */ + β”— πŸ“‚ myauthor /* Author name */ + β”— πŸ“‚ myplugin /* Plugin name */ + ┣ πŸ“‚ assets /* CSS, JavaScript and image assets for pages and components */ + ┃ ┣ πŸ“‚ css + ┃ ┣ πŸ“‚ favicons + ┃ ┣ πŸ“‚ images + ┃ ┣ πŸ“‚ js + ┃ β”— πŸ“‚ scss + ┣ πŸ“‚ behaviors /* Any custom behaviors provided by the plugin */ + ┣ πŸ“‚ classes /* Any custom classes provided by the plugin */ + ┣ πŸ“‚ components /* Components frontend */ + ┃ ┣ πŸ“‚ record /* Folder for the Record component's partials */ + ┃ ┃ β”— πŸ“œ default.htm /* The default partial that's rendered by the component */ + ┃ ┣ πŸ“‚ partials /* Any partials shared by more than one component in the plugin */ + ┃ β”— πŸ“œ Record.php /* Record Component that probably handles retrieving and displaying a single record */ + ┣ πŸ“‚ config /* Configuration files */ + ┃ β”— πŸ“œ config.php + ┣ πŸ“‚ console /* Any custom CLI commands provided by the plugin */ + ┣ πŸ“‚ controllers /* Backend controllers */ + ┃ ┣ πŸ“‚ records /* Directory for the view and configuration files for the given controller */ + ┃ ┃ ┣ πŸ“œ _list_toolbar.htm /* List toolbar partial file */ + ┃ ┃ ┣ πŸ“œ config_filter.yaml /* Configuration for the Filter widget present on the controller lists */ + ┃ ┃ ┣ πŸ“œ config_form.yaml /* Configuration for the Form widget present on the controller */ + ┃ ┃ ┣ πŸ“œ config_importexport.yaml /* Configuration for the Import/Export behavior */ + ┃ ┃ ┣ πŸ“œ config_list.yaml /* Configuration for the Lists widget present on the controller */ + ┃ ┃ ┣ πŸ“œ config_relation.yaml /* Configuration for the RelationController behavior */ + ┃ ┃ ┣ πŸ“œ create.htm /* View file for the create action */ + ┃ ┃ ┣ πŸ“œ index.htm /* View file for the index action */ + ┃ ┃ ┣ πŸ“œ preview.htm /* View file for the preview action */ + ┃ ┃ β”— πŸ“œ update.htm /* View file for the update action */ + ┃ β”— πŸ“œ Records.php /* Backend controller for the Record model */ + ┣ πŸ“‚ docs /* Any plugin-specific documentation should live here */ + ┣ πŸ“‚ formwidgets /* Any custom FormWidgets provided by the plugin */ + ┣ πŸ“‚ lang /* Localization files */ + ┃ β”— πŸ“‚ en /* Specific locale folder */ + ┃ β”— πŸ“œ lang.php /* Translations for that locale */ + ┣ πŸ“‚ layouts /* Any custom backend layouts used by the plugin */ + ┣ πŸ“‚ models /* Models provided by the plugin */ + ┃ ┣ πŸ“‚ record /* Directory containing configuration files specific to that model */ + ┃ ┃ ┣ πŸ“œ columns.yaml /* Configuration file used for the Lists widget */ + ┃ ┃ β”— πŸ“œ fields.yaml /* Configuration file used for the Form widget */ + ┃ β”— πŸ“œ Record.php /* Model class for the Record model */ + ┣ πŸ“‚ partials /* Any custom partials used by the plugin */ + ┣ πŸ“‚ reportwidgets /* Any custom ReportWidgets provided by the plugin */ + ┣ πŸ“‚ tests /* Test suite for the plugin */ + ┣ πŸ“‚ traits /* Any custom Traits provided by the plugin */ + ┣ πŸ“‚ updates /* Database migrations */ + ┃ ┃ β”— πŸ“‚ v1.0.0 /* Migrations for a specific version of the plugin */ + ┃ ┃ β”— πŸ“œ create_records_table.php /* Database migration file, referenced in version.yaml */ + ┃ β”— πŸ“œ version.yaml /* Changelog */ + ┣ πŸ“‚ views /* Custom view files */ + ┃ β”— πŸ“‚ mail /* Custom mail templates provided by the plugin */ + ┣ πŸ“‚ widgets /* Any custom Widgets provided by the plugin */ + ┣ πŸ“œ LICENSE /* License file */ + ┣ πŸ“œ README.md /* Documentation describing the purpose of the plugin */ + ┣ πŸ“œ Plugin.php /* Plugin registration file */ + ┣ πŸ“œ composer.json /* Composer file to manage dependencies for the plugin */ + ┣ πŸ“œ helpers.php /* Global helpers provided by the plugin loaded via composer.json */ + ┣ πŸ“œ phpunit.xml /* Unit testing configuration */ + ┣ πŸ“œ plugin.yaml /* Simplified plugin registration configuration YAML file, used by Builder plugin */ + β”— πŸ“œ routes.php /* Any custom routes provided by the plugin */ ``` @@ -190,38 +196,50 @@ class Plugin extends \System\Classes\PluginBase The following methods are supported in the plugin registration class: + +
    + Method | Description ------------- | ------------- -**pluginDetails()** | returns information about the plugin. -**register()** | register method, called when the plugin is first registered. -**boot()** | boot method, called right before the request route. -**registerComponents()** | registers any [frontend components](components#component-registration) used by this plugin. -**registerFormWidgets()** | registers any [backend form widgets](../backend/widgets#form-widget-registration) supplied by this plugin. -**registerListColumnTypes()** | registers any [custom list column types](../backend/lists#custom-column-types) supplied by this plugin. -**registerMailLayouts()** | registers any [mail view layouts](../services/mail#mail-template-registration) supplied by this plugin. -**registerMailPartials()** | registers any [mail view partials](../services/mail#mail-template-registration) supplied by this plugin. -**registerMailTemplates()** | registers any [mail view templates](../services/mail#mail-template-registration) supplied by this plugin. -**registerMarkupTags()** | registers [additional markup tags](#extending-twig) that can be used in the CMS. -**registerNavigation()** | registers [backend navigation menu items](#navigation-menus) for this plugin. -**registerPermissions()** | registers any [backend permissions](../backend/users#permission-registration) used by this plugin. -**registerReportWidgets()** | registers any [backend report widgets](../backend/widgets#report-widget-registration), including the dashboard widgets. -**registerSchedule()** | registers [scheduled tasks](../plugin/scheduling#defining-schedules) that are executed on a regular basis. -**registerSettings()** | registers any [backend configuration links](settings#link-registration) used by this plugin. -**registerValidationRules()** | registers any [custom validators](../services/validation#custom-validation-rules) supplied by this plugin. +`pluginDetails()` | returns information about the plugin. +`register()` | register method, called when the plugin is first registered. +`boot()` | boot method, called right before the request route. +`registerComponents()` | registers any [frontend components](components#component-registration) used by this plugin. +`registerFormWidgets()` | registers any [backend form widgets](../backend/widgets#form-widget-registration) supplied by this plugin. +`registerListColumnTypes()` | registers any [custom list column types](../backend/lists#custom-column-types) supplied by this plugin. +`registerMailLayouts()` | registers any [mail view layouts](../services/mail#mail-template-registration) supplied by this plugin. +`registerMailPartials()` | registers any [mail view partials](../services/mail#mail-template-registration) supplied by this plugin. +`registerMailTemplates()` | registers any [mail view templates](../services/mail#mail-template-registration) supplied by this plugin. +`registerMarkupTags()` | registers [additional markup tags](#extending-twig) that can be used in the CMS. +`registerNavigation()` | registers [backend navigation menu items](#navigation-menus) for this plugin. +`registerPermissions()` | registers any [backend permissions](../backend/users#permission-registration) used by this plugin. +`registerReportWidgets()` | registers any [backend report widgets](../backend/widgets#report-widget-registration), including the dashboard widgets. +`registerSchedule()` | registers [scheduled tasks](../plugin/scheduling#defining-schedules) that are executed on a regular basis. +`registerSettings()` | registers any [backend configuration links](settings#link-registration) used by this plugin. +`registerValidationRules()` | registers any [custom validators](../services/validation#custom-validation-rules) supplied by this plugin. ### Basic plugin information The `pluginDetails` is a required method of the plugin registration class. It should return an array containing the following keys: + +
    + Key | Description ------------- | ------------- -**name** | the plugin name, required. -**description** | the plugin description, required. -**author** | the plugin author name, required. -**icon** | a name of the plugin icon. The full list of available icons can be found in the [UI documentation](../ui/icon). Any icon names provided by this font are valid, for example **icon-glass**, **icon-music**. This key is required if `iconSvg` is not set. -**iconSvg** | an SVG icon to be used in place of the standard icon. The SVG icon should be a rectangle and can support colors. This key is required if `icon` is not set. -**homepage** | a link to the author's website address, optional. +`name` | the plugin name, required. +`description` | the plugin description, required. +`author` | the plugin author name, required. +`icon` | a name of the plugin icon. The full list of available icons can be found in the [UI documentation](../ui/icon). Any icon names provided by this font are valid, for example **icon-glass**, **icon-music**. This key is required if `iconSvg` is not set. +`iconSvg` | an SVG icon to be used in place of the standard icon. The SVG icon should be a rectangle and can support colors. This key is required if `icon` is not set. +`homepage` | a link to the author's website address, optional. ## Routing and initialization @@ -336,6 +354,14 @@ public function makeTextAllCaps($text) The following Twig custom options are available: + +
    + | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `needs_environment` | boolean | `false` | if true provides the current `TwigEnvironment` as the first argument to the filter call | @@ -395,19 +421,25 @@ When you register the backend navigation you can use [localization strings](loca To make the sub-menu items visible, you may [set the navigation context](../backend/controllers-ajax#navigation-context) in the backend controller using the `BackendMenu::setContext` method. This will make the parent menu item active and display the children in the side menu. + +
    + Key | Description ------------- | ------------- -**label** | specifies the menu label localization string key, required. -**icon** | an icon name from the [Winter CMS icon collection](../ui/icon), optional. -**iconSvg** | an SVG icon to be used in place of the standard icon, the SVG icon should be a rectangle and can support colors, optional. -**url** | the URL the menu item should point to (ex. `Backend::url('author/plugin/controller/action')`, required. -**counter** | a numeric value to output near the menu icon. The value should be a number or a callable returning a number, optional. -**counterLabel** | a string value to describe the numeric reference in counter, optional. -**badge** | a string value to output in place of the counter, the value should be a string and will override the badge property if set, optional. -**attributes** | an associative array of attributes and values to apply to the menu item, optional. -**permissions** | an array of permissions the backend user must have in order to view the menu item (Note: direct access of URLs still requires separate permission checks), optional. -**code** | a string value that acts as an unique identifier for that menu option. **NOTE**: This is a system generated value and should not be provided when registering the navigation items. -**owner** | a string value that specifies the menu items owner plugin or module in the format "Author.Plugin". **NOTE**: This is a system generated value and should not be provided when registering the navigation items. +`label` | specifies the menu label localization string key, required. +`icon` | an icon name from the [Winter CMS icon collection](../ui/icon), optional. +`iconSvg` | an SVG icon to be used in place of the standard icon, the SVG icon should be a rectangle and can support colors, optional. +`url` | the URL the menu item should point to (ex. `Backend::url('author/plugin/controller/action')`, required. +`counter` | a numeric value to output near the menu icon. The value should be a number or a callable returning a number, optional. +`counterLabel` | a string value to describe the numeric reference in counter, optional. +`badge` | a string value to output in place of the counter, the value should be a string and will override the badge property if set, optional. +`attributes` | an associative array of attributes and values to apply to the menu item, optional. +`permissions` | an array of permissions the backend user must have in order to view the menu item (Note: direct access of URLs still requires separate permission checks), optional. +`code` | a string value that acts as an unique identifier for that menu option. **NOTE**: This is a system generated value and should not be provided when registering the navigation items. +`owner` | a string value that specifies the menu items owner plugin or module in the format "Author.Plugin". **NOTE**: This is a system generated value and should not be provided when registering the navigation items. ## Registering middleware @@ -425,7 +457,7 @@ public function boot() Alternatively, you can push it directly into the Kernel via the following. -``` +```php public function boot() { // Add a new middleware to beginning of the stack. @@ -443,16 +475,22 @@ public function boot() By default plugins are restricted from accessing certain areas of the system. This is to prevent critical errors that may lock an administrator out from the backend. When these areas are accessed without elevated permissions, the `boot` and `register` [initialization methods](#routing-initialization) for the plugin will not fire. + +
    + Request | Description ------------- | ------------- -**/combine** | the asset combiner generator URL -**/backend/system/updates** | the site updates context -**/backend/system/install** | the installer path -**/backend/backend/auth** | the backend authentication path (login, logout) -**winter:up** | the CLI command that runs all pending migrations -**winter:update** | the CLI command that triggers the update process -**winter:env** | the CLI command that converts configuration files to environment variables in a `.env` file -**winter:version** | the CLI command that detects the version of Winter CMS that is installed +`/combine` | the asset combiner generator URL +`/backend/system/updates` | the site updates context +`/backend/system/install` | the installer path +`/backend/backend/auth` | the backend authentication path (login, logout) +`winter:up` | the CLI command that runs all pending migrations +`winter:update` | the CLI command that triggers the update process +`winter:env` | the CLI command that converts configuration files to environment variables in a `.env` file +`winter:version` | the CLI command that detects the version of Winter CMS that is installed Define the `$elevated` property to grant elevated permissions for your plugin. diff --git a/plugin-scheduling.md b/plugin-scheduling.md index ed6499ce..122d3b8c 100644 --- a/plugin-scheduling.md +++ b/plugin-scheduling.md @@ -21,25 +21,31 @@ You may define all of your scheduled tasks by overriding the `registerSchedule` To get started, let's look at an example of scheduling a task. In this example, we will schedule a `Closure` to be called every day at midnight. Within the `Closure` we will execute a database query to clear a table: - class Plugin extends PluginBase +```php +class Plugin extends PluginBase +{ + [...] + + public function registerSchedule($schedule) { - [...] - - public function registerSchedule($schedule) - { - $schedule->call(function () { - \Db::table('recent_users')->delete(); - })->daily(); - } + $schedule->call(function () { + \Db::table('recent_users')->delete(); + })->daily(); } +} +``` -In addition to scheduling `Closure` calls, you may also schedule [console commands](../console/commands) and operating system commands. For example, you may use the `command` method to schedule a console command: +In addition to scheduling `Closure` calls, you may also schedule [console commands](../console/introduction) and operating system commands. For example, you may use the `command` method to schedule a console command: - $schedule->command('cache:clear')->daily(); +```php +$schedule->command('cache:clear')->daily(); +``` The `exec` command may be used to issue a command to the operating system: - $schedule->exec('node /home/acme/script.js')->daily(); +```php +$schedule->exec('node /home/acme/script.js')->daily(); +``` ### Schedule frequency options @@ -62,9 +68,11 @@ Method | Description These methods may be combined with additional constraints to create even more finely tuned schedules that only run on certain days of the week. For example, to schedule a command to run weekly on Monday: - $schedule->call(function () { - // Runs once a week on Monday at 13:00... - })->weekly()->mondays()->at('13:00'); +```php +$schedule->call(function () { + // Runs once a week on Monday at 13:00... +})->weekly()->mondays()->at('13:00'); +``` Below is a list of the additional schedule constraints: @@ -84,34 +92,42 @@ Method | Description The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other words, if the given `Closure` return `true`, the task will execute as long as no other constraining conditions prevent the task from running: - $schedule->command('emails:send')->daily()->when(function () { - return true; - }); +```php +$schedule->command('emails:send')->daily()->when(function () { + return true; +}); +``` ### Preventing task overlaps By default, scheduled tasks will be run even if the previous instance of the task is still running. To prevent this, you may use the `withoutOverlapping` method: - $schedule->command('emails:send')->withoutOverlapping(); +```php +$schedule->command('emails:send')->withoutOverlapping(); +``` -In this example, the `emails:send` [console command](../console/commands) will be run every minute if it is not already running. The `withoutOverlapping` method is especially useful if you have tasks that vary drastically in their execution time, preventing you from needing to predict exactly how long a given task will take. +In this example, the `emails:send` [console command](../console/introduction) will be run every minute if it is not already running. The `withoutOverlapping` method is especially useful if you have tasks that vary drastically in their execution time, preventing you from needing to predict exactly how long a given task will take. ## Task output The scheduler provides several convenient methods for working with the output generated by scheduled tasks. First, using the `sendOutputTo` method, you may send the output to a file for later inspection: - $schedule->command('emails:send') - ->daily() - ->sendOutputTo($filePath); +```php +$schedule->command('emails:send') + ->daily() + ->sendOutputTo($filePath); +``` Using the `emailOutputTo` method, you may e-mail the output to an e-mail address of your choice. Note that the output must first be sent to a file using the `sendOutputTo` method. Also before e-mailing the output of a task, you should configure [mail services](../services/mail): - $schedule->command('foo') - ->daily() - ->sendOutputTo($filePath) - ->emailOutputTo('foo@example.com'); +```php +$schedule->command('foo') + ->daily() + ->sendOutputTo($filePath) + ->emailOutputTo('foo@example.com'); +``` > **NOTE:** The `emailOutputTo` and `sendOutputTo` methods are exclusive to the `command` method and are not supported for `call`. @@ -120,22 +136,26 @@ Using the `emailOutputTo` method, you may e-mail the output to an e-mail address Using the `before` and `after` methods, you may specify code to be executed before and after the scheduled task is complete: - $schedule->command('emails:send') - ->daily() - ->before(function () { - // Task is about to start... - }) - ->after(function () { - // Task is complete... - }); +```php +$schedule->command('emails:send') + ->daily() + ->before(function () { + // Task is about to start... + }) + ->after(function () { + // Task is complete... + }); +``` #### Pinging URLs Using the `pingBefore` and `thenPing` methods, the scheduler can automatically ping a given URL before or after a task is complete. This method is useful for notifying an external service that your scheduled task is commencing or complete: - $schedule->command('emails:send') - ->daily() - ->pingBefore($url) - ->thenPing($url); +```php +$schedule->command('emails:send') + ->daily() + ->pingBefore($url) + ->thenPing($url); +``` > You need to install [Drivers plugin](https://wintercms.com/plugin/winter-drivers) before you can use either the `pingBefore($url)` or `thenPing($url)` features. diff --git a/plugin-settings.md b/plugin-settings.md index 52cae47e..539dbf77 100644 --- a/plugin-settings.md +++ b/plugin-settings.md @@ -21,32 +21,36 @@ You can create models for storing settings in the database by implementing the ` The settings model classes should extend the Model class and implement the `System.Behaviors.SettingsModel` behavior. The settings models, like any other models, should be defined in the **models** subdirectory of the plugin directory. The model from the next example should be defined in the `plugins/acme/demo/models/Settings.php` script. - 'ABCD']); +// Set an array of values +Settings::set(['api_key' => 'ABCD']); - // Set object values - $settings = Settings::instance(); - $settings->api_key = 'ABCD'; - $settings->save(); +// Set object values +$settings = Settings::instance(); +$settings->api_key = 'ABCD'; +$settings->save(); +``` ### Reading from a settings model The settings model has the static `get` method that enables you to load individual properties. Also, when you instantiate a model with the `instance` method, it loads the properties from the database and you can access them directly. - // Outputs: ABCD - echo Settings::instance()->api_key; +```php +// Outputs: ABCD +echo Settings::instance()->api_key; - // Get a single value - echo Settings::get('api_key'); - - // Get a value and return a default value if it doesn't exist - echo Settings::get('is_activated', true); +// Get a single value +echo Settings::get('api_key'); +// Get a value and return a default value if it doesn't exist +echo Settings::get('is_activated', true); +``` ## Backend settings pages @@ -95,40 +102,44 @@ The backend contains a dedicated area for housing settings and configuration, it The backend settings navigation links can be extended by overriding the `registerSettings` method inside the [Plugin registration class](registration#registration-file). When you create a configuration link you have two options - create a link to a specific backend page, or create a link to a settings model. The next example shows how to create a link to a backend page. - public function registerSettings() - { - return [ - 'location' => [ - 'label' => 'Locations', - 'description' => 'Manage available user countries and states.', - 'category' => 'Users', - 'icon' => 'icon-globe', - 'url' => Backend::url('acme/user/locations'), - 'order' => 500, - 'keywords' => 'geography place placement' - ] - ]; - } +```php +public function registerSettings() +{ + return [ + 'location' => [ + 'label' => 'Locations', + 'description' => 'Manage available user countries and states.', + 'category' => 'Users', + 'icon' => 'icon-globe', + 'url' => Backend::url('acme/user/locations'), + 'order' => 500, + 'keywords' => 'geography place placement' + ] + ]; +} +``` > **NOTE:** Backend settings pages should [set the settings context](#settings-page-context) in order to mark the corresponding settings menu item active in the System page sidebar. Settings context for settings models is detected automatically. The following example creates a link to a settings model. Settings models is a part of the settings API which is described above in the [Database settings](#database-settings) section. - public function registerSettings() - { - return [ - 'settings' => [ - 'label' => 'User Settings', - 'description' => 'Manage user based settings.', - 'category' => 'Users', - 'icon' => 'icon-cog', - 'class' => 'Acme\User\Models\Settings', - 'order' => 500, - 'keywords' => 'security location', - 'permissions' => ['acme.users.access_settings'] - ] - ]; - } +```php +public function registerSettings() +{ + return [ + 'settings' => [ + 'label' => 'User Settings', + 'description' => 'Manage user based settings.', + 'category' => 'Users', + 'icon' => 'icon-cog', + 'class' => 'Acme\User\Models\Settings', + 'order' => 500, + 'keywords' => 'security location', + 'permissions' => ['acme.users.access_settings'] + ] + ]; +} +``` The optional `keywords` parameter is used by the settings search feature. If keywords are not provided, the search uses only the settings item label and description. @@ -137,15 +148,17 @@ The optional `keywords` parameter is used by the settings search feature. If key Just like [setting navigation context in the controller](../backend/controllers-ajax#navigation-context), Backend settings pages should set the settings navigation context. It's required in order to mark the current settings link in the System page sidebar as active. Use the `System\Classes\SettingsManager` class to set the settings context. Usually it could be done in the controller constructor: - public function __construct() - { - parent::__construct(); +```php +public function __construct() +{ + parent::__construct(); - [...] + [...] - BackendMenu::setContext('Winter.System', 'system', 'settings'); - SettingsManager::setContext('You.Plugin', 'settings'); - } + BackendMenu::setContext('Winter.System', 'system', 'settings'); + SettingsManager::setContext('You.Plugin', 'settings'); +} +``` The first argument of the `setContext` method is the settings item owner in the following format: **author.plugin**. The second argument is the setting name, the same as you provided when [registering the backend settings page](#link-registration). @@ -154,28 +167,38 @@ The first argument of the `setContext` method is the settings item owner in the Plugins can have a configuration file `config.php` in the `config` subdirectory of the plugin directory. The configuration files are PHP scripts that define and return an **array**. Example configuration file `plugins/acme/demo/config/config.php`: - 10, - 'display' => 5 - ]; +return [ + 'maxItems' => 10, + 'display' => 5 +]; +``` Use the `Config` class for accessing the configuration values defined in the configuration file. The `Config::get($name, $default = null)` method accepts the plugin and the parameter name in the following format: **Acme.Demo::maxItems**. The second optional parameter defines the default value to return if the configuration parameter doesn't exist. Example: - use Config; +```php +use Config; - ... +... - $maxItems = Config::get('acme.demo::maxItems', 50); +$maxItems = Config::get('acme.demo::maxItems', 50); +``` -A plugin configuration can be overridden by the application by creating a configuration file `config/author/plugin/config.php`, for example `config/acme/todo/config.php`, or `config/acme/todo/dev/config.php` for different environment. Inside the overridden configuration file you can return only values you want to override: +A plugin configuration can be overridden by the application by creating a configuration file `config/author/plugin/config.php`, for example `config/acme/todo/config.php`, or `config/acme/todo/dev/config.php` for an environment specific override (in this case `dev`). - **NOTE:** In order for the config override to work, the plugin must contain a default config file (i.e. `plugins/author/plugin/config/config.php`. Even if you expect all configuration to come from the project override instead of the default configuration file it is still **highly** recommended that a default configuration file is provided as a form of documentation as to what configuration options are available to be modified on the project level. - return [ - 'maxItems' => 20 - ]; +Inside the overridden configuration file you can return only values you want to override: + +```php + 20 +]; +``` If you want to use separate configurations across different environments (eg: **dev**, **production**), simply create another file in `config/author/plugin/environment/config.php`. Replace **environment** with the environment name. This will be merged with `config/author/plugin/config.php`. @@ -183,10 +206,12 @@ Example: **config/author/plugin/production/config.php:** - 25 - ]; +return [ + 'maxItems' => 25 +]; +``` This will set `maxItems` to 25 when `APP_ENV` is set to **production**. diff --git a/plugin-updates.md b/plugin-updates.md index 57823630..591db81c 100644 --- a/plugin-updates.md +++ b/plugin-updates.md @@ -16,13 +16,13 @@ The change log is stored in a YAML file called `version.yaml` inside the **/upda ```css πŸ“‚ plugins - ┣ πŸ“‚ myauthor <-- Author name - ┃ ┣ πŸ“‚ myplugin <-- Plugin name - ┃ ┃ ┣ πŸ“‚ updates <-- Database migrations - ┃ ┃ ┃ ┃ ┣ πŸ“‚ v1.0.0 <-- Migrations for a specific version of the plugin - ┃ ┃ ┃ ┃ ┃ ┣ πŸ“œ seed_the_database.php <-- Database seed file, referenced in version.yaml - ┃ ┃ ┃ ┃ ┃ β”— πŸ“œ create_records_table.php <-- Database migration file, referenced in version.yaml - ┃ ┃ ┃ β”— πŸ“œ version.yaml <-- Changelog + β”— πŸ“‚ myauthor <-- Author name + β”— πŸ“‚ myplugin <-- Plugin name + β”— πŸ“‚ updates <-- Database migrations + ┣ πŸ“‚ v1.0.0 <-- Migrations for a specific version of the plugin + ┃ ┣ πŸ“œ seed_the_database.php <-- Database seed file, referenced in version.yaml + ┃ β”— πŸ“œ create_records_table.php <-- Database migration file, referenced in version.yaml + β”— πŸ“œ version.yaml <-- Changelog ``` @@ -32,7 +32,7 @@ During an update the system will notify the user about recent changes to plugins 1. When an administrator signs in to the backend. 1. When the system is updated using the update feature in the backend area. -1. When the [console command](../console/commands#console-up-command) `php artisan winter:up` is called in the command line from the application directory. +1. When the [console command](../console/setup-maintenance#winter-up) `php artisan winter:up` is called in the command line from the application directory. > **NOTE:** The plugin [initialization process](../plugin/registration#routing-initialization) is disabled during the update process, this should be a consideration in migration and seeding scripts. diff --git a/services-application.md b/services-application.md index 935ab917..6f51aa27 100644 --- a/services-application.md +++ b/services-application.md @@ -14,13 +14,17 @@ The inversion of control (IoC) container is a tool for managing class dependenci There are two ways the IoC container can resolve dependencies: via Closure callbacks or automatic resolution. First, we'll explore Closure callbacks. First, a "type" may be bound into the container: - App::bind('foo', function($app) { - return new FooBar; - }); +```php +App::bind('foo', function($app) { + return new FooBar; +}); +``` #### Resolving a type from the container - $value = App::make('foo'); +```php +$value = App::make('foo'); +``` When the `App::make` method is called, the Closure callback is executed and the result is returned. @@ -28,27 +32,35 @@ When the `App::make` method is called, the Closure callback is executed and the Sometimes you may wish to bind something into the container that should only be resolved once, and the same instance should be returned on subsequent calls into the container: - App::singleton('foo', function() { - return new FooBar; - }); +```php +App::singleton('foo', function() { + return new FooBar; +}); +``` #### Binding an existing instance into the container You may also bind an existing object instance into the container using the `instance` method: - $foo = new Foo; +```php +$foo = new Foo; - App::instance('foo', $foo); +App::instance('foo', $foo); +``` #### Binding an interface to an implementation In some cases, a class may depend on an interface implementation, not a "concrete type". When this is the case, the `App::bind` method must be used to inform the container which interface implementation to inject: - App::bind('UserRepositoryInterface', 'DbUserRepository'); +```php +App::bind('UserRepositoryInterface', 'DbUserRepository'); +``` Now consider the following code: - $users = App::make('UserRepositoryInterface'); +```php +$users = App::make('UserRepositoryInterface'); +``` Since we have bound the `UserRepositoryInterface` to a concrete type, the `DbUserRepository` will automatically be injected into this controller when it is created. @@ -68,27 +80,31 @@ In fact, [plugin registration files](../plugin/registration) inherit service pro To create a service provider, simply extend the `Winter\Storm\Support\ServiceProvider` class and define a `register` method: - use Winter\Storm\Support\ServiceProvider; +```php +use Winter\Storm\Support\ServiceProvider; - class FooServiceProvider extends ServiceProvider - { - - public function register() - { - $this->app->bind('foo', function() { - return new Foo; - }); - } +class FooServiceProvider extends ServiceProvider +{ + public function register() + { + $this->app->bind('foo', function() { + return new Foo; + }); } +} +``` + Note that in the `register` method, the application IoC container is available to you via the `$this->app` property. Once you have created a provider and are ready to register it with your application, simply add it to the `providers` array in your `app` configuration file. #### Registering a service provider at run-time You may also register a service provider at run-time using the `App::register` method: - App::register('FooServiceProvider'); +```php +App::register('FooServiceProvider'); +``` ## Application events @@ -97,29 +113,33 @@ You may also register a service provider at run-time using the `App::register` m You can register special events before a requested is routed using the `before` and `after` methods: - App::before(function ($request) { - // Code to execute before the request is routed - }); +```php +App::before(function ($request) { + // Code to execute before the request is routed +}); - App::after(function ($request) { - // Code to execute after the request is routed - }); +App::after(function ($request) { + // Code to execute after the request is routed +}); +``` #### Container events The service container fires an event each time it resolves an object. You may listen to this event using the `resolving` method: - App::resolving(function ($object, $app) { - // Called when container resolves object of any type... - }); +```php +App::resolving(function ($object, $app) { + // Called when container resolves object of any type... +}); - App::resolving('foo', function ($fooBar, $app) { - // Called when container resolves objects using hint "foo"... - }); +App::resolving('foo', function ($fooBar, $app) { + // Called when container resolves objects using hint "foo"... +}); - App::resolving('Acme\Blog\Classes\FooBar', function ($fooBar, $app) { - // Called when container resolves objects of type "FooBar"... - }); +App::resolving('Acme\Blog\Classes\FooBar', function ($fooBar, $app) { + // Called when container resolves objects of type "FooBar"... +}); +``` As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consumer. @@ -130,15 +150,21 @@ As you can see, the object being resolved will be passed to the callback, allowi You may use the `environment` method to discover the application environment as determined by the [environment configuration](../setup/configuration#environment-config). - // production - App::environment(); +```php +// production +App::environment(); +``` #### Determine the execution context It is possible to know if the current request is being performed in the administrative backend area using the `runningInBackend` method. - App::runningInBackend(); +```php +App::runningInBackend(); +``` -You may also use the `runningInConsole` method to check if the executing code is taking place inside the [command line interface](../console/commands): +You may also use the `runningInConsole` method to check if the executing code is taking place inside the [command line interface](../console/introduction): - App::runningInConsole(); +```php +App::runningInConsole(); +``` diff --git a/services-asset-compilation.md b/services-asset-compilation.md index 5fb4593b..4079d203 100644 --- a/services-asset-compilation.md +++ b/services-asset-compilation.md @@ -103,15 +103,23 @@ Symbol | Description The asset combiner supports common aliases that substitute file paths, these will begin with the `@` symbol. For example the [AJAX framework assets](../ajax/introduction#framework-script) can be included in the combiner: - +```twig + +``` The following aliases are supported: + +
    + Alias | Description ------------- | ------------- `@jquery` | Reference to the jQuery library (v3.4.0) used in the backend. (JavaScript) @@ -149,7 +157,7 @@ If you are wanting to render the injected assets in any other context, you can c While the majority of the time dynamic asset compilation through `addJs()`, `addCss()`, or the [`| theme` filter](../markup/filter-theme) should be sufficient for your needs, you may occassionally have a complex asset compilation that you would like to just generate a static file on command instead of dynamically. -The Winter CMS core registers several such bundles for internal usage that are compiled whenever the [`artisan winter:util compile assets` command](../console/commands#winter-util-command) is run. +The Winter CMS core registers several such bundles for internal usage that are compiled whenever the [`artisan winter:util compile assets` command](../console/utilities#winter-util-compile-assets) is run. ## Extending the Asset Compiler @@ -169,6 +177,7 @@ CombineAssets::registerCallback(function ($combiner) { $this->registerAlias('jquery', '~/modules/backend/assets/js/vendor/jquery-and-migrate.min.js'); }); ``` + ### Register Custom Asset Bundles diff --git a/services-behaviors.md b/services-behaviors.md index 3f01f05a..76c395bd 100644 --- a/services-behaviors.md +++ b/services-behaviors.md @@ -245,11 +245,11 @@ This unique ability to extend constructors allows behaviors to be implemented dy ```php /** - * Extend the Winter.Users controller to include the RelationController behavior too + * Extend the Winter.Users Users controller to include the RelationController behavior too */ Winter\Users\Controllers\Users::extend(function($controller) { // Implement the list controller behavior dynamically - $controller->implement[] = 'Backend.Behaviors.RelationController'; + $controller->implement[] = \Backend\Behaviors\RelationController::class; // Declare the relationConfig property dynamically for the RelationController behavior to use $controller->addDynamicProperty('relationConfig', '$/myvendor/myplugin/controllers/users/config_relation.yaml'); @@ -338,7 +338,7 @@ echo $controller->asExtension('FormController')->otherMethod(); To check if an object has been extended with a behavior, you may use the `isClassExtendedWith` method on the object. ```php -$controller->isClassExtendedWith('Backend.Behaviors.RelationController'); +$controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class); ``` Below is an example of dynamically extending a `UsersController` of a third-party plugin utilizing this method to avoid preventing other plugins from also extending the afore-mentioned third-party plugin. @@ -346,8 +346,8 @@ Below is an example of dynamically extending a `UsersController` of a third-part ```php UsersController::extend(function($controller) { // Implement behavior if not already implemented - if (!$controller->isClassExtendedWith('Backend.Behaviors.RelationController')) { - $controller->implement[] = 'Backend.Behaviors.RelationController'; + if (!$controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class)) { + $controller->implement[] = \Backend\Behaviors\RelationController::class; } // Define property if not already defined diff --git a/services-collections.md b/services-collections.md index 998a3395..2101dac7 100644 --- a/services-collections.md +++ b/services-collections.md @@ -15,16 +15,18 @@ The `Winter\Storm\Support\Collection` class provides a fluent, convenient wrapper for working with arrays of data. For example, check out the following code. We'll create a new collection instance from the array, run the `strtoupper` function on each element, and then remove all empty elements: - $collection = new Winter\Storm\Support\Collection(['stewie', 'brian', null]); - - $collection = $collection - ->map(function ($name) { - return strtoupper($name); - }) - ->reject(function ($name) { - return empty($name); - }) - ; +```php +$collection = new Winter\Storm\Support\Collection(['stewie', 'brian', null]); + +$collection = $collection + ->map(function ($name) { + return strtoupper($name); + }) + ->reject(function ($name) { + return empty($name); + }) +; +``` The `Collection` class allows you to chain its methods to perform fluent mapping and reducing of the underlying array. In general every `Collection` method returns an entirely new `Collection` instance. @@ -33,7 +35,9 @@ The `Collection` class allows you to chain its methods to perform fluent mapping As described above, passing an array to the constructor of the `Winter\Storm\Support\Collection` class will return a new instance for the given array. So, creating a collection is as simple as: - $collection = new Winter\Storm\Support\Collection([1, 2, 3]); +```php +$collection = new Winter\Storm\Support\Collection([1, 2, 3]); +``` By default, collections of [database models](../database/model) are always returned as `Collection` instances; however, feel free to use the `Collection` class wherever it is convenient for your application. @@ -191,11 +195,13 @@ You may select any method from this table to see an example of its usage: The `all` method simply returns the underlying array represented by the collection: - $collection = new Collection([1, 2, 3]); +```php +$collection = new Collection([1, 2, 3]); - $collection->all(); +$collection->all(); - // [1, 2, 3] +// [1, 2, 3] +``` #### `average()` {#collection-method} @@ -207,92 +213,107 @@ Alias for the [`avg`](#method-avg) method. The `avg` method returns the [average value](https://en.wikipedia.org/wiki/Average) of a given key: - $average = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->avg('foo'); +```php +$average = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->avg('foo'); - // 20 +// 20 - $average = collect([1, 1, 2, 4])->avg(); +$average = collect([1, 1, 2, 4])->avg(); - // 2 +// 2 +``` #### `chunk()` {.collection-method} The `chunk` method breaks the collection into multiple, smaller collections of a given size: - $collection = new Collection([1, 2, 3, 4, 5, 6, 7]); +```php +$collection = new Collection([1, 2, 3, 4, 5, 6, 7]); - $chunks = $collection->chunk(4); +$chunks = $collection->chunk(4); - $chunks->toArray(); +$chunks->toArray(); - // [[1, 2, 3, 4], [5, 6, 7]] +// [[1, 2, 3, 4], [5, 6, 7]] +``` This method is especially useful in [CMS pages](../cms/pages) when working with a grid system, such as [Bootstrap](http://getbootstrap.com/css/#grid). Imagine you have a collection of models you want to display in a grid: - {% for chunk in products.chunk(3) %} -
    - {% for product in chunk %} -
    {{ product.name }}
    - {% endfor %} -
    - {% endfor %} +```twig +{% for chunk in products.chunk(3) %} +
    + {% for product in chunk %} +
    {{ product.name }}
    + {% endfor %} +
    +{% endfor %} +``` #### `collapse()` {.collection-method} The `collapse` method collapses a collection of arrays into a flat collection: - $collection = new Collection([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); +```php +$collection = new Collection([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + +$collapsed = $collection->collapse(); - $collapsed = $collection->collapse(); +$collapsed->all(); - $collapsed->all(); +// [1, 2, 3, 4, 5, 6, 7, 8, 9] +``` - // [1, 2, 3, 4, 5, 6, 7, 8, 9] #### `combine()` {#collection-method} The `combine` method combines the values of the collection, as keys, with the values of another array or collection: - $collection = collect(['name', 'age']); +```php +$collection = collect(['name', 'age']); - $combined = $collection->combine(['George', 29]); +$combined = $collection->combine(['George', 29]); - $combined->all(); +$combined->all(); - // ['name' => 'George', 'age' => 29] +// ['name' => 'George', 'age' => 29] +``` #### `collect()` {#collection-method} The `collect` method returns a new `Collection` instance with the items currently in the collection: - $collectionA = collect([1, 2, 3]); +```php +$collectionA = collect([1, 2, 3]); - $collectionB = $collectionA->collect(); +$collectionB = $collectionA->collect(); - $collectionB->all(); +$collectionB->all(); - // [1, 2, 3] +// [1, 2, 3] +``` The `collect` method is primarily useful for converting [lazy collections](#lazy-collections) into standard `Collection` instances: - $lazyCollection = LazyCollection::make(function () { - yield 1; - yield 2; - yield 3; - }); +```php +$lazyCollection = LazyCollection::make(function () { + yield 1; + yield 2; + yield 3; +}); - $collection = $lazyCollection->collect(); +$collection = $lazyCollection->collect(); - get_class($collection); +get_class($collection); - // 'Illuminate\Support\Collection' +// 'Illuminate\Support\Collection' - $collection->all(); +$collection->all(); - // [1, 2, 3] +// [1, 2, 3] +``` > **Tip:** The `collect` method is especially useful when you have an instance of `Enumerable` and need a non-lazy collection instance. Since `collect()` is part of the `Enumerable` contract, you can safely use it to get a `Collection` instance. @@ -301,49 +322,57 @@ The `collect` method is primarily useful for converting [lazy collections](#lazy The `concat` method appends the given `array` or collection values onto the end of the collection: - $collection = collect(['John Doe']); +```php +$collection = collect(['John Doe']); - $concatenated = $collection->concat(['Jane Doe'])->concat(['name' => 'Johnny Doe']); +$concatenated = $collection->concat(['Jane Doe'])->concat(['name' => 'Johnny Doe']); - $concatenated->all(); +$concatenated->all(); - // ['John Doe', 'Jane Doe', 'Johnny Doe'] +// ['John Doe', 'Jane Doe', 'Johnny Doe'] +``` #### `contains()` {#collection-method} The `contains` method determines whether the collection contains a given item: - $collection = collect(['name' => 'Desk', 'price' => 100]); +```php +$collection = collect(['name' => 'Desk', 'price' => 100]); - $collection->contains('Desk'); +$collection->contains('Desk'); - // true +// true - $collection->contains('New York'); +$collection->contains('New York'); - // false +// false +``` You may also pass a key / value pair to the `contains` method, which will determine if the given pair exists in the collection: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 100], +]); - $collection->contains('product', 'Bookcase'); +$collection->contains('product', 'Bookcase'); - // false +// false +``` Finally, you may also pass a callback to the `contains` method to perform your own truth test: - $collection = collect([1, 2, 3, 4, 5]); +```php +$collection = collect([1, 2, 3, 4, 5]); - $collection->contains(function ($value, $key) { - return $value > 5; - }); +$collection->contains(function ($value, $key) { + return $value > 5; +}); - // false +// false +``` The `contains` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`containsStrict`](#method-containsstrict) method to filter using "strict" comparisons. @@ -357,93 +386,103 @@ This method has the same signature as the [`contains`](#method-contains) method; The `count` method returns the total number of items in the collection: - $collection = new Collection([1, 2, 3, 4]); +```php +$collection = new Collection([1, 2, 3, 4]); - $collection->count(); +$collection->count(); - // 4 +// 4 +``` #### `countBy()` {#collection-method} The `countBy` method counts the occurrences of values in the collection. By default, the method counts the occurrences of every element: - $collection = collect([1, 2, 2, 2, 3]); +```php +$collection = collect([1, 2, 2, 2, 3]); - $counted = $collection->countBy(); +$counted = $collection->countBy(); - $counted->all(); +$counted->all(); - // [1 => 1, 2 => 3, 3 => 1] +// [1 => 1, 2 => 3, 3 => 1] +``` However, you pass a callback to the `countBy` method to count all items by a custom value: - $collection = collect(['alice@gmail.com', 'bob@yahoo.com', 'carlos@gmail.com']); +```php +$collection = collect(['alice@gmail.com', 'bob@yahoo.com', 'carlos@gmail.com']); - $counted = $collection->countBy(function ($email) { - return substr(strrchr($email, "@"), 1); - }); +$counted = $collection->countBy(function ($email) { + return substr(strrchr($email, "@"), 1); +}); - $counted->all(); +$counted->all(); - // ['gmail.com' => 2, 'yahoo.com' => 1] +// ['gmail.com' => 2, 'yahoo.com' => 1] +``` #### `crossJoin()` {#collection-method} The `crossJoin` method cross joins the collection's values among the given arrays or collections, returning a Cartesian product with all possible permutations: - $collection = collect([1, 2]); +```php +$collection = collect([1, 2]); - $matrix = $collection->crossJoin(['a', 'b']); +$matrix = $collection->crossJoin(['a', 'b']); - $matrix->all(); +$matrix->all(); - /* - [ - [1, 'a'], - [1, 'b'], - [2, 'a'], - [2, 'b'], - ] - */ +/* + [ + [1, 'a'], + [1, 'b'], + [2, 'a'], + [2, 'b'], + ] +*/ - $collection = collect([1, 2]); +$collection = collect([1, 2]); - $matrix = $collection->crossJoin(['a', 'b'], ['I', 'II']); +$matrix = $collection->crossJoin(['a', 'b'], ['I', 'II']); - $matrix->all(); +$matrix->all(); - /* - [ - [1, 'a', 'I'], - [1, 'a', 'II'], - [1, 'b', 'I'], - [1, 'b', 'II'], - [2, 'a', 'I'], - [2, 'a', 'II'], - [2, 'b', 'I'], - [2, 'b', 'II'], - ] - */ +/* + [ + [1, 'a', 'I'], + [1, 'a', 'II'], + [1, 'b', 'I'], + [1, 'b', 'II'], + [2, 'a', 'I'], + [2, 'a', 'II'], + [2, 'b', 'I'], + [2, 'b', 'II'], + ] +*/ +``` #### `dd()` {#collection-method} The `dd` method dumps the collection's items and ends execution of the script: - $collection = collect(['John Doe', 'Jane Doe']); +```php +$collection = collect(['John Doe', 'Jane Doe']); - $collection->dd(); +$collection->dd(); - /* - Collection { - #items: array:2 [ - 0 => "John Doe" - 1 => "Jane Doe" - ] - } - */ +/* + Collection { + #items: array:2 [ + 0 => "John Doe" + 1 => "Jane Doe" + ] + } +*/ +``` If you do not want to stop executing the script, use the [`dump`](#method-dump) method instead. @@ -452,77 +491,85 @@ If you do not want to stop executing the script, use the [`dump`](#method-dump) The `diff` method compares the collection against another collection or a plain PHP `array`: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $diff = $collection->diff([2, 4, 6, 8]); +$diff = $collection->diff([2, 4, 6, 8]); - $diff->all(); +$diff->all(); - // [1, 3, 5] +// [1, 3, 5] +``` #### `diffAssoc()` {#collection-method} The `diffAssoc` method compares the collection against another collection or a plain PHP `array` based on its keys and values. This method will return the key / value pairs in the original collection that are not present in the given collection: - $collection = collect([ - 'color' => 'orange', - 'type' => 'fruit', - 'remain' => 6 - ]); +```php +$collection = collect([ + 'color' => 'orange', + 'type' => 'fruit', + 'remain' => 6 +]); - $diff = $collection->diffAssoc([ - 'color' => 'yellow', - 'type' => 'fruit', - 'remain' => 3, - 'used' => 6, - ]); +$diff = $collection->diffAssoc([ + 'color' => 'yellow', + 'type' => 'fruit', + 'remain' => 3, + 'used' => 6, +]); - $diff->all(); +$diff->all(); - // ['color' => 'orange', 'remain' => 6] +// ['color' => 'orange', 'remain' => 6] +``` #### `diffKeys()` {#collection-method} The `diffKeys` method compares the collection against another collection or a plain PHP `array` based on its keys. This method will return the key / value pairs in the original collection that are not present in the given collection: - $collection = collect([ - 'one' => 10, - 'two' => 20, - 'three' => 30, - 'four' => 40, - 'five' => 50, - ]); +```php +$collection = collect([ + 'one' => 10, + 'two' => 20, + 'three' => 30, + 'four' => 40, + 'five' => 50, +]); - $diff = $collection->diffKeys([ - 'two' => 2, - 'four' => 4, - 'six' => 6, - 'eight' => 8, - ]); +$diff = $collection->diffKeys([ + 'two' => 2, + 'four' => 4, + 'six' => 6, + 'eight' => 8, +]); - $diff->all(); +$diff->all(); - // ['one' => 10, 'three' => 30, 'five' => 50] +// ['one' => 10, 'three' => 30, 'five' => 50] +``` #### `dump()` {#collection-method} The `dump` method dumps the collection's items: - $collection = collect(['John Doe', 'Jane Doe']); +```php +$collection = collect(['John Doe', 'Jane Doe']); - $collection->dump(); +$collection->dump(); - /* - Collection { - #items: array:2 [ - 0 => "John Doe" - 1 => "Jane Doe" - ] - } - */ +/* + Collection { + #items: array:2 [ + 0 => "John Doe" + 1 => "Jane Doe" + ] + } +*/ +``` If you want to stop executing the script after dumping the collection, use the [`dd`](#method-dd) method instead. @@ -531,23 +578,27 @@ If you want to stop executing the script after dumping the collection, use the [ The `duplicates` method retrieves and returns duplicate values from the collection: - $collection = collect(['a', 'b', 'a', 'c', 'b']); +```php +$collection = collect(['a', 'b', 'a', 'c', 'b']); - $collection->duplicates(); +$collection->duplicates(); - // [2 => 'a', 4 => 'b'] +// [2 => 'a', 4 => 'b'] +``` If the collection contains arrays or objects, you can pass the key of the attributes that you wish to check for duplicate values: - $employees = collect([ - ['email' => 'abigail@example.com', 'position' => 'Developer'], - ['email' => 'james@example.com', 'position' => 'Designer'], - ['email' => 'victoria@example.com', 'position' => 'Developer'], - ]) +```php +$employees = collect([ + ['email' => 'abigail@example.com', 'position' => 'Developer'], + ['email' => 'james@example.com', 'position' => 'Designer'], + ['email' => 'victoria@example.com', 'position' => 'Developer'], +]) - $employees->duplicates('position'); +$employees->duplicates('position'); - // [2 => 'Developer'] +// [2 => 'Developer'] +``` #### `duplicatesStrict()` {#collection-method} @@ -559,50 +610,59 @@ This method has the same signature as the [`duplicates`](#method-duplicates) met The `each` method iterates over the items in the collection and passes each item to a callback: - $collection->each(function ($item, $key) { - // - }); +```php +$collection->each(function ($item, $key) { + // +}); +``` If you would like to stop iterating through the items, you may return `false` from your callback: - $collection->each(function ($item, $key) { - if (/* some condition */) { - return false; - } - }); - +```php +$collection->each(function ($item, $key) { + if (/* some condition */) { + return false; + } +}); +``` #### `every()` {.collection-method} The `every` method creates a new collection consisting of every n-th element: - $collection = new Collection(['a', 'b', 'c', 'd', 'e', 'f']); +```php +$collection = new Collection(['a', 'b', 'c', 'd', 'e', 'f']); - $collection->every(4); +$collection->every(4); - // ['a', 'e'] +// ['a', 'e'] +``` You may optionally pass offset as the second argument: - $collection->every(4, 1); +```php +$collection->every(4, 1); - // ['b', 'f'] +// ['b', 'f'] +``` #### `filter()` {.collection-method} The `filter` method filters the collection by a given callback, keeping only those items that pass a given truth test: - $collection = new Collection([1, 2, 3, 4]); +```php +$collection = new Collection([1, 2, 3, 4]); - $filtered = $collection->filter(function ($item) { - return $item > 2; - }); +$filtered = $collection->filter(function ($item) { + return $item > 2; +}); - $filtered->all(); +$filtered->all(); - // [3, 4] +// [3, 4] +``` For the inverse of `filter`, see the [reject](#method-reject) method. @@ -611,103 +671,121 @@ For the inverse of `filter`, see the [reject](#method-reject) method. The `first` method returns the first element in the collection that passes a given truth test: - new Collection([1, 2, 3, 4])->first(function ($value, $key) { - return $value > 2; - }); +```php +new Collection([1, 2, 3, 4])->first(function ($value, $key) { + return $value > 2; +}); - // 3 +// 3 +``` You may also call the `first` method with no arguments to get the first element in the collection. If the collection is empty, `null` is returned: - new Collection([1, 2, 3, 4])->first(); +```php +new Collection([1, 2, 3, 4])->first(); - // 1 +// 1 +``` #### `firstWhere()` {#collection-method} The `firstWhere` method returns the first element in the collection with the given key / value pair: - $collection = collect([ - ['name' => 'Regena', 'age' => null], - ['name' => 'Linda', 'age' => 14], - ['name' => 'Diego', 'age' => 23], - ['name' => 'Linda', 'age' => 84], - ]); +```php +$collection = collect([ + ['name' => 'Regena', 'age' => null], + ['name' => 'Linda', 'age' => 14], + ['name' => 'Diego', 'age' => 23], + ['name' => 'Linda', 'age' => 84], +]); - $collection->firstWhere('name', 'Linda'); +$collection->firstWhere('name', 'Linda'); - // ['name' => 'Linda', 'age' => 14] +// ['name' => 'Linda', 'age' => 14] +``` You may also call the `firstWhere` method with an operator: - $collection->firstWhere('age', '>=', 18); +```php +$collection->firstWhere('age', '>=', 18); - // ['name' => 'Diego', 'age' => 23] +// ['name' => 'Diego', 'age' => 23] +``` Like the [where](#method-where) method, you may pass one argument to the `firstWhere` method. In this scenario, the `firstWhere` method will return the first item where the given item key's value is "truthy": - $collection->firstWhere('age'); +```php +$collection->firstWhere('age'); - // ['name' => 'Linda', 'age' => 14] +// ['name' => 'Linda', 'age' => 14] +``` #### `flatMap()` {#collection-method} The `flatMap` method iterates through the collection and passes each value to the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items. Then, the array is flattened by a level: - $collection = collect([ - ['name' => 'Sally'], - ['school' => 'Arkansas'], - ['age' => 28] - ]); +```php +$collection = collect([ + ['name' => 'Sally'], + ['school' => 'Arkansas'], + ['age' => 28] +]); - $flattened = $collection->flatMap(function ($values) { - return array_map('strtoupper', $values); - }); +$flattened = $collection->flatMap(function ($values) { + return array_map('strtoupper', $values); +}); - $flattened->all(); +$flattened->all(); - // ['name' => 'SALLY', 'school' => 'ARKANSAS', 'age' => '28']; +// ['name' => 'SALLY', 'school' => 'ARKANSAS', 'age' => '28']; +``` #### `flatten()` {.collection-method} The `flatten` method flattens a multi-dimensional collection into a single dimension: - $collection = new Collection(['name' => 'peter', 'languages' => ['php', 'javascript']]); +```php +$collection = new Collection(['name' => 'peter', 'languages' => ['php', 'javascript']]); - $flattened = $collection->flatten(); +$flattened = $collection->flatten(); - $flattened->all(); +$flattened->all(); - // ['peter', 'php', 'javascript']; +// ['peter', 'php', 'javascript']; +``` #### `flip()` {.collection-method} The `flip` method swaps the collection's keys with their corresponding values: - $collection = new Collection(['name' => 'peter', 'platform' => 'winter']); +```php +$collection = new Collection(['name' => 'peter', 'platform' => 'winter']); - $flipped = $collection->flip(); +$flipped = $collection->flip(); - $flipped->all(); +$flipped->all(); - // ['peter' => 'name', 'winter' => 'platform'] +// ['peter' => 'name', 'winter' => 'platform'] +``` #### `forget()` {.collection-method} The `forget` method removes an item from the collection by its key: - $collection = new Collection(['name' => 'peter', 'platform' => 'winter']); +```php +$collection = new Collection(['name' => 'peter', 'platform' => 'winter']); - $collection->forget('name'); +$collection->forget('name'); - $collection->all(); +$collection->all(); - // ['platform' => 'winter'] +// ['platform' => 'winter'] +``` > **NOTE:** Unlike most other collection methods, `forget` does not return a new modified collection; it modifies the collection it is called on. @@ -716,11 +794,13 @@ The `forget` method removes an item from the collection by its key: The `forPage` method returns a new collection containing the items that would be present on a given page number: - $collection = new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9])->forPage(2, 3); +```php +$collection = new Collection([1, 2, 3, 4, 5, 6, 7, 8, 9])->forPage(2, 3); - $collection->all(); +$collection->all(); - // [4, 5, 6] +// [4, 5, 6] +``` The method requires the page number and the number of items to show per page, respectively. @@ -729,85 +809,97 @@ The method requires the page number and the number of items to show per page, re The `get` method returns the item at a given key. If the key does not exist, `null` is returned: - $collection = new Collection(['name' => 'peter', 'platform' => 'winter']); +```php +$collection = new Collection(['name' => 'peter', 'platform' => 'winter']); - $value = $collection->get('name'); +$value = $collection->get('name'); - // peter +// peter +``` You may optionally pass a default value as the second argument: - $collection = new Collection(['name' => 'peter', 'platform' => 'winter']); +```php +$collection = new Collection(['name' => 'peter', 'platform' => 'winter']); - $value = $collection->get('foo', 'default-value'); +$value = $collection->get('foo', 'default-value'); - // default-value +// default-value +``` You may even pass a callback as the default value. The result of the callback will be returned if the specified key does not exist: - $collection->get('email', function () { - return 'default-value'; - }); +```php +$collection->get('email', function () { + return 'default-value'; +}); - // default-value +// default-value +``` #### `groupBy()` {.collection-method} The `groupBy` method groups the collection's items by a given key: - $collection = new Collection([ - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ['account_id' => 'account-x10', 'product' => 'Chair'], - ['account_id' => 'account-x11', 'product' => 'Desk'], - ]); - - $grouped = $collection->groupBy('account_id'); - - $grouped->toArray(); - - /* - [ - 'account-x10' => [ - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ['account_id' => 'account-x10', 'product' => 'Chair'], - ], - 'account-x11' => [ - ['account_id' => 'account-x11', 'product' => 'Desk'], - ], - ] - */ +```php +$collection = new Collection([ + ['account_id' => 'account-x10', 'product' => 'Bookcase'], + ['account_id' => 'account-x10', 'product' => 'Chair'], + ['account_id' => 'account-x11', 'product' => 'Desk'], +]); + +$grouped = $collection->groupBy('account_id'); + +$grouped->toArray(); + +/* + [ + 'account-x10' => [ + ['account_id' => 'account-x10', 'product' => 'Bookcase'], + ['account_id' => 'account-x10', 'product' => 'Chair'], + ], + 'account-x11' => [ + ['account_id' => 'account-x11', 'product' => 'Desk'], + ], + ] +*/ +``` In addition to passing a string `key`, you may also pass a callback. The callback should return the value you wish to key the group by: - $grouped = $collection->groupBy(function ($item, $key) { - return substr($item['account_id'], -3); - }); - - $grouped->toArray(); - - /* - [ - 'x10' => [ - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ['account_id' => 'account-x10', 'product' => 'Chair'], - ], - 'x11' => [ - ['account_id' => 'account-x11', 'product' => 'Desk'], - ], - ] - */ +```php +$grouped = $collection->groupBy(function ($item, $key) { + return substr($item['account_id'], -3); +}); + +$grouped->toArray(); + +/* + [ + 'x10' => [ + ['account_id' => 'account-x10', 'product' => 'Bookcase'], + ['account_id' => 'account-x10', 'product' => 'Chair'], + ], + 'x11' => [ + ['account_id' => 'account-x11', 'product' => 'Desk'], + ], + ] +*/ +``` #### `has()` {.collection-method} The `has` method determines if a given key exists in the collection: - $collection = new Collection(['account_id' => 1, 'product' => 'Desk']); +```php +$collection = new Collection(['account_id' => 1, 'product' => 'Desk']); - $collection->has('email'); +$collection->has('email'); - // false +// false +``` #### `implode()` {.collection-method} @@ -816,33 +908,39 @@ The `implode` method joins the items in a collection. Its arguments depend on th If the collection contains arrays or objects, you should pass the key of the attributes you wish to join, and the "glue" string you wish to place between the values: - $collection = new Collection([ - ['account_id' => 1, 'product' => 'Chair'], - ['account_id' => 2, 'product' => 'Desk'], - ]); +```php +$collection = new Collection([ + ['account_id' => 1, 'product' => 'Chair'], + ['account_id' => 2, 'product' => 'Desk'], +]); - $collection->implode('product', ', '); +$collection->implode('product', ', '); - // Chair, Desk +// Chair, Desk +``` If the collection contains simple strings or numeric values, simply pass the "glue" as the only argument to the method: - new Collection([1, 2, 3, 4, 5])->implode('-'); +```php +new Collection([1, 2, 3, 4, 5])->implode('-'); - // '1-2-3-4-5' +// '1-2-3-4-5' +``` #### `intersect()` {.collection-method} The `intersect` method removes any values that are not present in the given `array` or collection: - $collection = new Collection(['Desk', 'Sofa', 'Chair']); +```php +$collection = new Collection(['Desk', 'Sofa', 'Chair']); - $intersect = $collection->intersect(['Desk', 'Chair', 'Bookcase']); +$intersect = $collection->intersect(['Desk', 'Chair', 'Bookcase']); - $intersect->all(); +$intersect->all(); - // [0 => 'Desk', 2 => 'Chair'] +// [0 => 'Desk', 2 => 'Chair'] +``` As you can see, the resulting collection will preserve the original collection's keys. @@ -851,133 +949,152 @@ As you can see, the resulting collection will preserve the original collection's The `intersectByKeys` method removes any keys from the original collection that are not present in the given `array` or collection: - $collection = collect([ - 'serial' => 'UX301', 'type' => 'screen', 'year' => 2009 - ]); +```php +$collection = collect([ + 'serial' => 'UX301', 'type' => 'screen', 'year' => 2009 +]); - $intersect = $collection->intersectByKeys([ - 'reference' => 'UX404', 'type' => 'tab', 'year' => 2011 - ]); +$intersect = $collection->intersectByKeys([ + 'reference' => 'UX404', 'type' => 'tab', 'year' => 2011 +]); - $intersect->all(); +$intersect->all(); - // ['type' => 'screen', 'year' => 2009] +// ['type' => 'screen', 'year' => 2009] +``` #### `isEmpty()` {.collection-method} The `isEmpty` method returns `true` if the collection is empty; otherwise `false` is returned: - new Collection([])->isEmpty(); +```php +new Collection([])->isEmpty(); - // true +// true +``` #### `isNotEmpty()` {#collection-method} The `isNotEmpty` method returns `true` if the collection is not empty; otherwise, `false` is returned: - collect([])->isNotEmpty(); +```php +collect([])->isNotEmpty(); - // false +// false +``` #### `join()` {#collection-method} The `join` method joins the collection's values with a string: - collect(['a', 'b', 'c'])->join(', '); // 'a, b, c' - collect(['a', 'b', 'c'])->join(', ', ', and '); // 'a, b, and c' - collect(['a', 'b'])->join(', ', ' and '); // 'a and b' - collect(['a'])->join(', ', ' and '); // 'a' - collect([])->join(', ', ' and '); // '' +```php +collect(['a', 'b', 'c'])->join(', '); // 'a, b, c' +collect(['a', 'b', 'c'])->join(', ', ', and '); // 'a, b, and c' +collect(['a', 'b'])->join(', ', ' and '); // 'a and b' +collect(['a'])->join(', ', ' and '); // 'a' +collect([])->join(', ', ' and '); // '' +``` #### `keyBy()` {.collection-method} Keys the collection by the given key: - $collection = new Collection([ - ['product_id' => 'prod-100', 'name' => 'chair'], - ['product_id' => 'prod-200', 'name' => 'desk'], - ]); +```php +$collection = new Collection([ + ['product_id' => 'prod-100', 'name' => 'chair'], + ['product_id' => 'prod-200', 'name' => 'desk'], +]); - $keyed = $collection->keyBy('product_id'); +$keyed = $collection->keyBy('product_id'); - $keyed->all(); +$keyed->all(); - /* - [ - 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], - 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], - ] - */ +/* + [ + 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], + 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], + ] +*/ +``` If multiple items have the same key, only the last one will appear in the new collection. You may also pass your own callback, which should return the value to key the collection by: - $keyed = $collection->keyBy(function ($item) { - return strtoupper($item['product_id']); - }); +```php +$keyed = $collection->keyBy(function ($item) { + return strtoupper($item['product_id']); +}); - $keyed->all(); - - /* - [ - 'PROD-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], - 'PROD-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], - ] - */ +$keyed->all(); +/* + [ + 'PROD-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], + 'PROD-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], + ] +*/ +``` #### `keys()` {.collection-method} The `keys` method returns all of the collection's keys: - $collection = new Collection([ - 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], - 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], - ]); +```php +$collection = new Collection([ + 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Chair'], + 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Desk'], +]); - $keys = $collection->keys(); +$keys = $collection->keys(); - $keys->all(); +$keys->all(); - // ['prod-100', 'prod-200'] +// ['prod-100', 'prod-200'] +``` #### `last()` {.collection-method} The `last` method returns the last element in the collection that passes a given truth test: - new Collection([1, 2, 3, 4])->last(function ($key, $value) { - return $value < 3; - }); +```php +new Collection([1, 2, 3, 4])->last(function ($key, $value) { + return $value < 3; +}); - // 2 +// 2 +``` You may also call the `last` method with no arguments to get the last element in the collection. If the collection is empty then `null` is returned. - new Collection([1, 2, 3, 4])->last(); +```php +new Collection([1, 2, 3, 4])->last(); - // 4 +// 4 +``` #### `map()` {.collection-method} The `map` method iterates through the collection and passes each value to the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $multiplied = $collection->map(function ($item, $key) { - return $item * 2; - }); +$multiplied = $collection->map(function ($item, $key) { + return $item * 2; +}); - $multiplied->all(); +$multiplied->all(); - // [2, 4, 6, 8, 10] +// [2, 4, 6, 8, 10] +``` > **NOTE:** Like most other collection methods, `map` returns a new collection instance; it does not modify the collection it is called on. If you want to transform the original collection, use the [`transform`](#method-transform) method. @@ -986,230 +1103,258 @@ The `map` method iterates through the collection and passes each value to the gi The `mapInto()` method iterates over the collection, creating a new instance of the given class by passing the value into the constructor: - class Currency +```php +class Currency +{ + /** + * Create a new currency instance. + * + * @param string $code + * @return void + */ + function __construct(string $code) { - /** - * Create a new currency instance. - * - * @param string $code - * @return void - */ - function __construct(string $code) - { - $this->code = $code; - } + $this->code = $code; } +} - $collection = collect(['USD', 'EUR', 'GBP']); +$collection = collect(['USD', 'EUR', 'GBP']); - $currencies = $collection->mapInto(Currency::class); +$currencies = $collection->mapInto(Currency::class); - $currencies->all(); +$currencies->all(); - // [Currency('USD'), Currency('EUR'), Currency('GBP')] +// [Currency('USD'), Currency('EUR'), Currency('GBP')] +``` #### `mapSpread()` {#collection-method} The `mapSpread` method iterates over the collection's items, passing each nested item value into the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items: - $collection = collect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); +```php +$collection = collect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - $chunks = $collection->chunk(2); +$chunks = $collection->chunk(2); - $sequence = $chunks->mapSpread(function ($even, $odd) { - return $even + $odd; - }); +$sequence = $chunks->mapSpread(function ($even, $odd) { + return $even + $odd; +}); - $sequence->all(); +$sequence->all(); - // [1, 5, 9, 13, 17] +// [1, 5, 9, 13, 17] +``` #### `mapToGroups()` {#collection-method} The `mapToGroups` method groups the collection's items by the given callback. The callback should return an associative array containing a single key / value pair, thus forming a new collection of grouped values: - $collection = collect([ - [ - 'name' => 'John Doe', - 'department' => 'Sales', - ], - [ - 'name' => 'Jane Doe', - 'department' => 'Sales', - ], - [ - 'name' => 'Johnny Doe', - 'department' => 'Marketing', - ] - ]); - - $grouped = $collection->mapToGroups(function ($item, $key) { - return [$item['department'] => $item['name']]; - }); - - $grouped->toArray(); - - /* - [ - 'Sales' => ['John Doe', 'Jane Doe'], - 'Marketing' => ['Johnny Doe'], - ] - */ - - $grouped->get('Sales')->all(); - - // ['John Doe', 'Jane Doe'] +```php +$collection = collect([ + [ + 'name' => 'John Doe', + 'department' => 'Sales', + ], + [ + 'name' => 'Jane Doe', + 'department' => 'Sales', + ], + [ + 'name' => 'Johnny Doe', + 'department' => 'Marketing', + ] +]); + +$grouped = $collection->mapToGroups(function ($item, $key) { + return [$item['department'] => $item['name']]; +}); + +$grouped->toArray(); + +/* + [ + 'Sales' => ['John Doe', 'Jane Doe'], + 'Marketing' => ['Johnny Doe'], + ] +*/ + +$grouped->get('Sales')->all(); + +// ['John Doe', 'Jane Doe'] +``` #### `mapWithKeys()` {#collection-method} The `mapWithKeys` method iterates through the collection and passes each value to the given callback. The callback should return an associative array containing a single key / value pair: - $collection = collect([ - [ - 'name' => 'John', - 'department' => 'Sales', - 'email' => 'john@example.com' - ], - [ - 'name' => 'Jane', - 'department' => 'Marketing', - 'email' => 'jane@example.com' - ] - ]); - - $keyed = $collection->mapWithKeys(function ($item) { - return [$item['email'] => $item['name']]; - }); - - $keyed->all(); - - /* - [ - 'john@example.com' => 'John', - 'jane@example.com' => 'Jane', - ] - */ +```php +$collection = collect([ + [ + 'name' => 'John', + 'department' => 'Sales', + 'email' => 'john@example.com' + ], + [ + 'name' => 'Jane', + 'department' => 'Marketing', + 'email' => 'jane@example.com' + ] +]); + +$keyed = $collection->mapWithKeys(function ($item) { + return [$item['email'] => $item['name']]; +}); + +$keyed->all(); + +/* + [ + 'john@example.com' => 'John', + 'jane@example.com' => 'Jane', + ] +*/ +``` #### `max()` {#collection-method} The `max` method returns the maximum value of a given key: - $max = collect([['foo' => 10], ['foo' => 20]])->max('foo'); +```php +$max = collect([['foo' => 10], ['foo' => 20]])->max('foo'); - // 20 +// 20 - $max = collect([1, 2, 3, 4, 5])->max(); +$max = collect([1, 2, 3, 4, 5])->max(); - // 5 +// 5 +``` #### `median()` {#collection-method} The `median` method returns the [median value](https://en.wikipedia.org/wiki/Median) of a given key: - $median = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->median('foo'); +```php +$median = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->median('foo'); - // 15 +// 15 - $median = collect([1, 1, 2, 4])->median(); +$median = collect([1, 1, 2, 4])->median(); - // 1.5 +// 1.5 +``` #### `merge()` {#collection-method} The `merge` method merges the given array or collection with the original collection. If a string key in the given items matches a string key in the original collection, the given items's value will overwrite the value in the original collection: - $collection = collect(['product_id' => 1, 'price' => 100]); +```php +$collection = collect(['product_id' => 1, 'price' => 100]); - $merged = $collection->merge(['price' => 200, 'discount' => false]); +$merged = $collection->merge(['price' => 200, 'discount' => false]); - $merged->all(); +$merged->all(); - // ['product_id' => 1, 'price' => 200, 'discount' => false] +// ['product_id' => 1, 'price' => 200, 'discount' => false] +``` If the given items's keys are numeric, the values will be appended to the end of the collection: - $collection = collect(['Desk', 'Chair']); +```php +$collection = collect(['Desk', 'Chair']); - $merged = $collection->merge(['Bookcase', 'Door']); +$merged = $collection->merge(['Bookcase', 'Door']); - $merged->all(); +$merged->all(); - // ['Desk', 'Chair', 'Bookcase', 'Door'] +// ['Desk', 'Chair', 'Bookcase', 'Door'] +``` #### `mergeRecursive()` {#collection-method} The `mergeRecursive` method merges the given array or collection recursively with the original collection. If a string key in the given items matches a string key in the original collection, then the values for these keys are merged together into an array, and this is done recursively: - $collection = collect(['product_id' => 1, 'price' => 100]); +```php +$collection = collect(['product_id' => 1, 'price' => 100]); - $merged = $collection->mergeRecursive(['product_id' => 2, 'price' => 200, 'discount' => false]); +$merged = $collection->mergeRecursive(['product_id' => 2, 'price' => 200, 'discount' => false]); - $merged->all(); +$merged->all(); - // ['product_id' => [1, 2], 'price' => [100, 200], 'discount' => false] +// ['product_id' => [1, 2], 'price' => [100, 200], 'discount' => false] +``` #### `min()` {#collection-method} The `min` method returns the minimum value of a given key: - $min = collect([['foo' => 10], ['foo' => 20]])->min('foo'); +```php +$min = collect([['foo' => 10], ['foo' => 20]])->min('foo'); - // 10 +// 10 - $min = collect([1, 2, 3, 4, 5])->min(); +$min = collect([1, 2, 3, 4, 5])->min(); - // 1 +// 1 +``` #### `mode()` {#collection-method} The `mode` method returns the [mode value](https://en.wikipedia.org/wiki/Mode_(statistics)) of a given key: - $mode = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->mode('foo'); +```php +$mode = collect([['foo' => 10], ['foo' => 10], ['foo' => 20], ['foo' => 40]])->mode('foo'); - // [10] +// [10] - $mode = collect([1, 1, 2, 4])->mode(); +$mode = collect([1, 1, 2, 4])->mode(); - // [1] +// [1] +``` #### `nth()` {#collection-method} The `nth` method creates a new collection consisting of every n-th element: - $collection = collect(['a', 'b', 'c', 'd', 'e', 'f']); +```php +$collection = collect(['a', 'b', 'c', 'd', 'e', 'f']); - $collection->nth(4); +$collection->nth(4); - // ['a', 'e'] +// ['a', 'e'] +``` You may optionally pass an offset as the second argument: - $collection->nth(4, 1); +```php +$collection->nth(4, 1); - // ['b', 'f'] +// ['b', 'f'] +``` #### `only()` {#collection-method} The `only` method returns the items in the collection with the specified keys: - $collection = collect(['product_id' => 1, 'name' => 'Desk', 'price' => 100, 'discount' => false]); +```php +$collection = collect(['product_id' => 1, 'name' => 'Desk', 'price' => 100, 'discount' => false]); - $filtered = $collection->only(['product_id', 'name']); +$filtered = $collection->only(['product_id', 'name']); - $filtered->all(); +$filtered->all(); - // ['product_id' => 1, 'name' => 'Desk'] +// ['product_id' => 1, 'name' => 'Desk'] +``` For the inverse of `only`, see the [except](#method-except) method. @@ -1220,199 +1365,229 @@ The `pad` method will fill the array with the given value until the array reache To pad to the left, you should specify a negative size. No padding will take place if the absolute value of the given size is less than or equal to the length of the array: - $collection = collect(['A', 'B', 'C']); +```php +$collection = collect(['A', 'B', 'C']); - $filtered = $collection->pad(5, 0); +$filtered = $collection->pad(5, 0); - $filtered->all(); +$filtered->all(); - // ['A', 'B', 'C', 0, 0] +// ['A', 'B', 'C', 0, 0] - $filtered = $collection->pad(-5, 0); +$filtered = $collection->pad(-5, 0); - $filtered->all(); +$filtered->all(); - // [0, 0, 'A', 'B', 'C'] +// [0, 0, 'A', 'B', 'C'] +``` #### `partition()` {#collection-method} The `partition` method may be combined with the `list` PHP function to separate elements that pass a given truth test from those that do not: - $collection = collect([1, 2, 3, 4, 5, 6]); +```php +$collection = collect([1, 2, 3, 4, 5, 6]); - list($underThree, $equalOrAboveThree) = $collection->partition(function ($i) { - return $i < 3; - }); +list($underThree, $equalOrAboveThree) = $collection->partition(function ($i) { + return $i < 3; +}); - $underThree->all(); +$underThree->all(); - // [1, 2] +// [1, 2] - $equalOrAboveThree->all(); +$equalOrAboveThree->all(); - // [3, 4, 5, 6] +// [3, 4, 5, 6] +``` #### `pipe()` {#collection-method} The `pipe` method passes the collection to the given callback and returns the result: - $collection = collect([1, 2, 3]); +```php +$collection = collect([1, 2, 3]); - $piped = $collection->pipe(function ($collection) { - return $collection->sum(); - }); +$piped = $collection->pipe(function ($collection) { + return $collection->sum(); +}); - // 6 +// 6 +``` #### `pluck()` {.collection-method} The `pluck` method retrieves all of the collection values for a given key: - $collection = new Collection([ - ['product_id' => 'prod-100', 'name' => 'Chair'], - ['product_id' => 'prod-200', 'name' => 'Desk'], - ]); +```php +$collection = new Collection([ + ['product_id' => 'prod-100', 'name' => 'Chair'], + ['product_id' => 'prod-200', 'name' => 'Desk'], +]); - $plucked = $collection->pluck('name'); +$plucked = $collection->pluck('name'); - $plucked->all(); +$plucked->all(); - // ['Chair', 'Desk'] +// ['Chair', 'Desk'] +``` You may also specify how you wish the resulting collection to be keyed: - $plucked = $collection->pluck('name', 'product_id'); +```php +$plucked = $collection->pluck('name', 'product_id'); - $plucked->all(); +$plucked->all(); - // ['prod-100' => 'Desk', 'prod-200' => 'Chair'] +// ['prod-100' => 'Desk', 'prod-200' => 'Chair'] +``` #### `pop()` {.collection-method} The `pop` method removes and returns the last item from the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $collection->pop(); +$collection->pop(); - // 5 +// 5 - $collection->all(); +$collection->all(); - // [1, 2, 3, 4] +// [1, 2, 3, 4] +``` #### `prepend()` {.collection-method} The `prepend` method adds an item to the beginning of the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $collection->prepend(0); +$collection->prepend(0); - $collection->all(); +$collection->all(); - // [0, 1, 2, 3, 4, 5] +// [0, 1, 2, 3, 4, 5] +``` #### `pull()` {.collection-method} The `pull` method removes and returns an item from the collection by its key: - $collection = new Collection(['product_id' => 'prod-100', 'name' => 'Desk']); +```php +$collection = new Collection(['product_id' => 'prod-100', 'name' => 'Desk']); - $collection->pull('name'); +$collection->pull('name'); - // 'Desk' +// 'Desk' - $collection->all(); +$collection->all(); - // ['product_id' => 'prod-100'] +// ['product_id' => 'prod-100'] +``` #### `push()` {.collection-method} The `push` method appends an item to the end of the collection: - $collection = new Collection([1, 2, 3, 4]); +```php +$collection = new Collection([1, 2, 3, 4]); - $collection->push(5); +$collection->push(5); - $collection->all(); +$collection->all(); - // [1, 2, 3, 4, 5] +// [1, 2, 3, 4, 5] +``` #### `put()` {.collection-method} The `put` method sets the given key and value in the collection: - $collection = new Collection(['product_id' => 1, 'name' => 'Desk']); +```php +$collection = new Collection(['product_id' => 1, 'name' => 'Desk']); - $collection->put('price', 100); +$collection->put('price', 100); - $collection->all(); +$collection->all(); - // ['product_id' => 1, 'name' => 'Desk', 'price' => 100] +// ['product_id' => 1, 'name' => 'Desk', 'price' => 100] +``` #### `random()` {.collection-method} The `random` method returns a random item from the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $collection->random(); +$collection->random(); - // 4 - (retrieved randomly) +// 4 - (retrieved randomly) +``` You may optionally pass an integer to `random`. If that integer is more than `1`, a collection of items is returned: - $random = $collection->random(3); +```php +$random = $collection->random(3); - $random->all(); +$random->all(); - // [2, 4, 5] - (retrieved randomly) +// [2, 4, 5] - (retrieved randomly) +``` #### `reduce()` {.collection-method} The `reduce` method reduces the collection to a single value, passing the result of each iteration into the subsequent iteration: - $collection = new Collection([1, 2, 3]); +```php +$collection = new Collection([1, 2, 3]); - $total = $collection->reduce(function ($carry, $item) { - return $carry + $item; - }); +$total = $collection->reduce(function ($carry, $item) { + return $carry + $item; +}); - // 6 +// 6 +``` The value for `$carry` on the first iteration is `null`; however, you may specify its initial value by passing a second argument to `reduce`: - $collection->reduce(function ($carry, $item) { - return $carry + $item; - }, 4); +```php +$collection->reduce(function ($carry, $item) { + return $carry + $item; +}, 4); - // 10 +// 10 +``` #### `reject()` {.collection-method} The `reject` method filters the collection using the given callback. The callback should return `true` for any items it wishes to remove from the resulting collection: - $collection = new Collection([1, 2, 3, 4]); +```php +$collection = new Collection([1, 2, 3, 4]); - $filtered = $collection->reject(function ($item) { - return $item > 2; - }); +$filtered = $collection->reject(function ($item) { + return $item > 2; +}); - $filtered->all(); +$filtered->all(); - // [1, 2] +// [1, 2] +``` For the inverse of the `reject` method, see the [`filter`](#method-filter) method. @@ -1421,127 +1596,148 @@ For the inverse of the `reject` method, see the [`filter`](#method-filter) metho The `replace` method behaves similarly to `merge`; however, in addition to overwriting matching items with string keys, the `replace` method will also overwrite items in the collection that have matching numeric keys: - $collection = collect(['Taylor', 'Abigail', 'James']); +```php +$collection = collect(['Taylor', 'Abigail', 'James']); - $replaced = $collection->replace([1 => 'Victoria', 3 => 'Finn']); +$replaced = $collection->replace([1 => 'Victoria', 3 => 'Finn']); - $replaced->all(); +$replaced->all(); - // ['Taylor', 'Victoria', 'James', 'Finn'] +// ['Taylor', 'Victoria', 'James', 'Finn'] +``` #### `replaceRecursive()` {#collection-method} This method works like `replace`, but it will recur into arrays and apply the same replacement process to the inner values: - $collection = collect(['Taylor', 'Abigail', ['James', 'Victoria', 'Finn']]); +```php +$collection = collect(['Taylor', 'Abigail', ['James', 'Victoria', 'Finn']]); - $replaced = $collection->replaceRecursive(['Charlie', 2 => [1 => 'King']]); +$replaced = $collection->replaceRecursive(['Charlie', 2 => [1 => 'King']]); - $replaced->all(); +$replaced->all(); - // ['Charlie', 'Abigail', ['James', 'King', 'Finn']] +// ['Charlie', 'Abigail', ['James', 'King', 'Finn']] +``` #### `reverse()` {.collection-method} The `reverse` method reverses the order of the collection's items: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $reversed = $collection->reverse(); +$reversed = $collection->reverse(); - $reversed->all(); +$reversed->all(); - // [5, 4, 3, 2, 1] +// [5, 4, 3, 2, 1] +``` #### `search()` {.collection-method} The `search` method searches the collection for the given value and returns its key if found. If the item is not found, `false` is returned. - $collection = new Collection([2, 4, 6, 8]); +```php +$collection = new Collection([2, 4, 6, 8]); - $collection->search(4); +$collection->search(4); - // 1 +// 1 +``` The search is done using a "loose" comparison. To use strict comparison, pass `true` as the second argument to the method: - $collection->search('4', true); +```php +$collection->search('4', true); - // false +// false +``` Alternatively, you may pass in your own callback to search for the first item that passes your truth test: - $collection->search(function ($item, $key) { - return $item > 5; - }); +```php +$collection->search(function ($item, $key) { + return $item > 5; +}); - // 2 +// 2 +``` #### `shift()` {.collection-method} The `shift` method removes and returns the first item from the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $collection->shift(); +$collection->shift(); - // 1 +// 1 - $collection->all(); +$collection->all(); - // [2, 3, 4, 5] +// [2, 3, 4, 5] +``` #### `shuffle()` {.collection-method} The `shuffle` method randomly shuffles the items in the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $shuffled = $collection->shuffle(); +$shuffled = $collection->shuffle(); - $shuffled->all(); - - // [3, 2, 5, 1, 4] (generated randomly) +$shuffled->all(); +// [3, 2, 5, 1, 4] (generated randomly) +``` #### `skip()` {#collection-method} The `skip` method returns a new collection, without the first given amount of items: - $collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +```php +$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - $collection = $collection->skip(4); +$collection = $collection->skip(4); - $collection->all(); +$collection->all(); - // [5, 6, 7, 8, 9, 10] +// [5, 6, 7, 8, 9, 10] +``` #### `slice()` {#collection-method} The `slice` method returns a slice of the collection starting at the given index: - $collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +```php +$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - $slice = $collection->slice(4); +$slice = $collection->slice(4); - $slice->all(); +$slice->all(); - // [5, 6, 7, 8, 9, 10] +// [5, 6, 7, 8, 9, 10] +``` If you would like to limit the size of the returned slice, pass the desired size as the second argument to the method: - $slice = $collection->slice(4, 2); +```php +$slice = $collection->slice(4, 2); - $slice->all(); +$slice->all(); - // [5, 6] +// [5, 6] +``` The returned slice will preserve keys by default. If you do not wish to preserve the original keys, you can use the [`values`](#method-values) method to reindex them. @@ -1555,13 +1751,15 @@ Alias for the [`contains`](#method-contains) method. The `sort` method sorts the collection: - $collection = new Collection([5, 3, 1, 2, 4]); +```php +$collection = new Collection([5, 3, 1, 2, 4]); - $sorted = $collection->sort(); +$sorted = $collection->sort(); - $sorted->values()->all(); +$sorted->values()->all(); - // [1, 2, 3, 4, 5] +// [1, 2, 3, 4, 5] +``` The sorted collection keeps the original array keys. In this example we used the [`values`](#method-values) method to reset the keys to consecutively numbered indexes. @@ -1574,47 +1772,51 @@ If your sorting needs are more advanced, you may pass a callback to `sort` with The `sortBy` method sorts the collection by the given key: - $collection = new Collection([ - ['name' => 'Desk', 'price' => 200], - ['name' => 'Chair', 'price' => 100], - ['name' => 'Bookcase', 'price' => 150], - ]); +```php +$collection = new Collection([ + ['name' => 'Desk', 'price' => 200], + ['name' => 'Chair', 'price' => 100], + ['name' => 'Bookcase', 'price' => 150], +]); - $sorted = $collection->sortBy('price'); +$sorted = $collection->sortBy('price'); - $sorted->values()->all(); +$sorted->values()->all(); - /* - [ - ['name' => 'Chair', 'price' => 100], - ['name' => 'Bookcase', 'price' => 150], - ['name' => 'Desk', 'price' => 200], - ] - */ +/* + [ + ['name' => 'Chair', 'price' => 100], + ['name' => 'Bookcase', 'price' => 150], + ['name' => 'Desk', 'price' => 200], + ] +*/ +``` The sorted collection keeps the original array keys. In this example we used the [`values`](#method-values) method to reset the keys to consecutively numbered indexes. You can also pass your own callback to determine how to sort the collection values: - $collection = new Collection([ - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], - ]); +```php +$collection = new Collection([ + ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], + ['name' => 'Chair', 'colors' => ['Black']], + ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], +]); - $sorted = $collection->sortBy(function ($product, $key) { - return count($product['colors']); - }); +$sorted = $collection->sortBy(function ($product, $key) { + return count($product['colors']); +}); - $sorted->values()->all(); +$sorted->values()->all(); - /* - [ - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], - ] - */ +/* + [ + ['name' => 'Chair', 'colors' => ['Black']], + ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], + ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], + ] +*/ +``` #### `sortByDesc()` {.collection-method} @@ -1626,23 +1828,25 @@ This method has the same signature as the [`sortBy`](#method-sortby) method, but The `sortKeys` method sorts the collection by the keys of the underlying associative array: - $collection = collect([ - 'id' => 22345, - 'first' => 'John', - 'last' => 'Doe', - ]); +```php +$collection = collect([ + 'id' => 22345, + 'first' => 'John', + 'last' => 'Doe', +]); - $sorted = $collection->sortKeys(); +$sorted = $collection->sortKeys(); - $sorted->all(); +$sorted->all(); - /* - [ - 'first' => 'John', - 'id' => 22345, - 'last' => 'Doe', - ] - */ +/* + [ + 'first' => 'John', + 'id' => 22345, + 'last' => 'Doe', + ] +*/ +``` #### `sortKeysDesc()` {#collection-method} @@ -1654,218 +1858,250 @@ This method has the same signature as the [`sortKeys`](#method-sortkeys) method, The `splice` method removes and returns a slice of items starting at the specified index: - $collection = collect([1, 2, 3, 4, 5]); +```php +$collection = collect([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2); +$chunk = $collection->splice(2); - $chunk->all(); +$chunk->all(); - // [3, 4, 5] +// [3, 4, 5] - $collection->all(); +$collection->all(); - // [1, 2] +// [1, 2] +``` You may pass a second argument to limit the size of the resulting chunk: - $collection = collect([1, 2, 3, 4, 5]); +```php +$collection = collect([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2, 1); +$chunk = $collection->splice(2, 1); - $chunk->all(); +$chunk->all(); - // [3] +// [3] - $collection->all(); +$collection->all(); - // [1, 2, 4, 5] +// [1, 2, 4, 5] +``` In addition, you can pass a third argument containing the new items to replace the items removed from the collection: - $collection = collect([1, 2, 3, 4, 5]); +```php +$collection = collect([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2, 1, [10, 11]); +$chunk = $collection->splice(2, 1, [10, 11]); - $chunk->all(); +$chunk->all(); - // [3] +// [3] - $collection->all(); +$collection->all(); - // [1, 2, 10, 11, 4, 5] +// [1, 2, 10, 11, 4, 5] +``` #### `splice()` {.collection-method} The `splice` method removes and returns a slice of items starting at the specified index: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2); +$chunk = $collection->splice(2); - $chunk->all(); +$chunk->all(); - // [3, 4, 5] +// [3, 4, 5] - $collection->all(); +$collection->all(); - // [1, 2] +// [1, 2] +``` You may pass a second argument to limit the size of the resulting chunk: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2, 1); +$chunk = $collection->splice(2, 1); - $chunk->all(); +$chunk->all(); - // [3] +// [3] - $collection->all(); +$collection->all(); - // [1, 2, 4, 5] +// [1, 2, 4, 5] +``` In addition, you can pass a third argument containing the new items to replace the items removed from the collection: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $chunk = $collection->splice(2, 1, [10, 11]); +$chunk = $collection->splice(2, 1, [10, 11]); - $chunk->all(); +$chunk->all(); - // [3] +// [3] - $collection->all(); +$collection->all(); - // [1, 2, 10, 11, 4, 5] +// [1, 2, 10, 11, 4, 5] +``` #### `split()` {#collection-method} The `split` method breaks a collection into the given number of groups: - $collection = collect([1, 2, 3, 4, 5]); +```php +$collection = collect([1, 2, 3, 4, 5]); - $groups = $collection->split(3); +$groups = $collection->split(3); - $groups->toArray(); +$groups->toArray(); - // [[1, 2], [3, 4], [5]] +// [[1, 2], [3, 4], [5]] +``` #### `sum()` {.collection-method} The `sum` method returns the sum of all items in the collection: - new Collection([1, 2, 3, 4, 5])->sum(); +```php +new Collection([1, 2, 3, 4, 5])->sum(); - // 15 +// 15 +``` If the collection contains nested arrays or objects, you should pass a key to use for determining which values to sum: - $collection = new Collection([ - ['name' => 'JavaScript: The Good Parts', 'pages' => 176], - ['name' => 'JavaScript: The Definitive Guide', 'pages' => 1096], - ]); +```php +$collection = new Collection([ + ['name' => 'JavaScript: The Good Parts', 'pages' => 176], + ['name' => 'JavaScript: The Definitive Guide', 'pages' => 1096], +]); - $collection->sum('pages'); +$collection->sum('pages'); - // 1272 +// 1272 +``` In addition, you may pass your own callback to determine which values of the collection to sum: - $collection = new Collection([ - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], - ]); +```php +$collection = new Collection([ + ['name' => 'Chair', 'colors' => ['Black']], + ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], + ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], +]); - $collection->sum(function ($product) { - return count($product['colors']); - }); +$collection->sum(function ($product) { + return count($product['colors']); +}); - // 6 +// 6 +``` #### `take()` {.collection-method} The `take` method returns a new collection with the specified number of items: - $collection = new Collection([0, 1, 2, 3, 4, 5]); +```php +$collection = new Collection([0, 1, 2, 3, 4, 5]); - $chunk = $collection->take(3); +$chunk = $collection->take(3); - $chunk->all(); +$chunk->all(); - // [0, 1, 2] +// [0, 1, 2] +``` You may also pass a negative integer to take the specified amount of items from the end of the collection: - $collection = new Collection([0, 1, 2, 3, 4, 5]); +```php +$collection = new Collection([0, 1, 2, 3, 4, 5]); - $chunk = $collection->take(-2); +$chunk = $collection->take(-2); - $chunk->all(); +$chunk->all(); - // [4, 5] +// [4, 5] +``` #### `tap()` {#collection-method} The `tap` method passes the collection to the given callback, allowing you to "tap" into the collection at a specific point and do something with the items while not affecting the collection itself: - collect([2, 4, 3, 1, 5]) - ->sort() - ->tap(function ($collection) { - Log::debug('Values after sorting', $collection->values()->toArray()); - }) - ->shift(); +```php +collect([2, 4, 3, 1, 5]) + ->sort() + ->tap(function ($collection) { + Log::debug('Values after sorting', $collection->values()->toArray()); + }) + ->shift(); - // 1 +// 1 +``` #### `times()` {#collection-method} The static `times` method creates a new collection by invoking the callback a given amount of times: - $collection = Collection::times(10, function ($number) { - return $number * 9; - }); +```php +$collection = Collection::times(10, function ($number) { + return $number * 9; +}); - $collection->all(); +$collection->all(); - // [9, 18, 27, 36, 45, 54, 63, 72, 81, 90] +// [9, 18, 27, 36, 45, 54, 63, 72, 81, 90] +``` This method can be useful when combined with factories to create Eloquent models: - $categories = Collection::times(3, function ($number) { - return factory(Category::class)->create(['name' => "Category No. $number"]); - }); +```php +$categories = Collection::times(3, function ($number) { + return factory(Category::class)->create(['name' => "Category No. $number"]); +}); - $categories->all(); +$categories->all(); - /* - [ - ['id' => 1, 'name' => 'Category No. 1'], - ['id' => 2, 'name' => 'Category No. 2'], - ['id' => 3, 'name' => 'Category No. 3'], - ] - */ +/* + [ + ['id' => 1, 'name' => 'Category No. 1'], + ['id' => 2, 'name' => 'Category No. 2'], + ['id' => 3, 'name' => 'Category No. 3'], + ] +*/ +``` #### `toArray()` {.collection-method} The `toArray` method converts the collection into a plain PHP `array`. If the collection's values are [database models](../database/model), the models will also be converted to arrays: - $collection = new Collection(['name' => 'Desk', 'price' => 200]); +```php +$collection = new Collection(['name' => 'Desk', 'price' => 200]); - $collection->toArray(); +$collection->toArray(); - /* - [ - ['name' => 'Desk', 'price' => 200], - ] - */ +/* + [ + ['name' => 'Desk', 'price' => 200], + ] +*/ +``` > **NOTE:** `toArray` also converts all of its nested objects to an array. If you want to get the underlying array as is, use the [`all`](#method-all) method instead. @@ -1874,26 +2110,30 @@ The `toArray` method converts the collection into a plain PHP `array`. If the co The `toJson` method converts the collection into JSON: - $collection = new Collection(['name' => 'Desk', 'price' => 200]); +```php +$collection = new Collection(['name' => 'Desk', 'price' => 200]); - $collection->toJson(); +$collection->toJson(); - // '{"name":"Desk","price":200}' +// '{"name":"Desk","price":200}' +``` #### `transform()` {.collection-method} The `transform` method iterates over the collection and calls the given callback with each item in the collection. The items in the collection will be replaced by the values returned by the callback: - $collection = new Collection([1, 2, 3, 4, 5]); +```php +$collection = new Collection([1, 2, 3, 4, 5]); - $collection->transform(function ($item, $key) { - return $item * 2; - }); +$collection->transform(function ($item, $key) { + return $item * 2; +}); - $collection->all(); +$collection->all(); - // [2, 4, 6, 8, 10] +// [2, 4, 6, 8, 10] +``` > **NOTE:** Unlike most other collection methods, `transform` modifies the collection itself. If you wish to create a new collection instead, use the [`map`](#method-map) method. @@ -1902,64 +2142,72 @@ The `transform` method iterates over the collection and calls the given callback The `union` method adds the given array to the collection. If the given array contains keys that are already in the original collection, the original collection's values will be preferred: - $collection = collect([1 => ['a'], 2 => ['b']]); +```php +$collection = collect([1 => ['a'], 2 => ['b']]); - $union = $collection->union([3 => ['c'], 1 => ['b']]); +$union = $collection->union([3 => ['c'], 1 => ['b']]); - $union->all(); +$union->all(); - // [1 => ['a'], 2 => ['b'], 3 => ['c']] +// [1 => ['a'], 2 => ['b'], 3 => ['c']] +``` #### `unique()` {#collection-method} The `unique` method returns all of the unique items in the collection. The returned collection keeps the original array keys, so in this example we'll use the [`values`](#method-values) method to reset the keys to consecutively numbered indexes: - $collection = collect([1, 1, 2, 2, 3, 4, 2]); +```php +$collection = collect([1, 1, 2, 2, 3, 4, 2]); - $unique = $collection->unique(); +$unique = $collection->unique(); - $unique->values()->all(); +$unique->values()->all(); - // [1, 2, 3, 4] +// [1, 2, 3, 4] +``` When dealing with nested arrays or objects, you may specify the key used to determine uniqueness: - $collection = collect([ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], - ]); +```php +$collection = collect([ + ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], + ['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'], + ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], + ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], + ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], +]); - $unique = $collection->unique('brand'); +$unique = $collection->unique('brand'); - $unique->values()->all(); +$unique->values()->all(); - /* - [ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ] - */ +/* + [ + ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], + ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], + ] +*/ +``` You may also pass your own callback to determine item uniqueness: - $unique = $collection->unique(function ($item) { - return $item['brand'].$item['type']; - }); +```php +$unique = $collection->unique(function ($item) { + return $item['brand'].$item['type']; +}); - $unique->values()->all(); +$unique->values()->all(); - /* - [ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], - ] - */ +/* + [ + ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], + ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], + ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], + ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], + ] +*/ +``` The `unique` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`uniqueStrict`](#method-uniquestrict) method to filter using "strict" comparisons. @@ -1973,19 +2221,21 @@ This method has the same signature as the [`unique`](#method-unique) method; how The `unless` method will execute the given callback unless the first argument given to the method evaluates to `true`: - $collection = collect([1, 2, 3]); +```php +$collection = collect([1, 2, 3]); - $collection->unless(true, function ($collection) { - return $collection->push(4); - }); +$collection->unless(true, function ($collection) { + return $collection->push(4); +}); - $collection->unless(false, function ($collection) { - return $collection->push(5); - }); +$collection->unless(false, function ($collection) { + return $collection->push(5); +}); - $collection->all(); +$collection->all(); - // [1, 2, 3, 5] +// [1, 2, 3, 5] +``` For the inverse of `unless`, see the [`when`](#method-when) method. @@ -2004,58 +2254,63 @@ Alias for the [`whenEmpty`](#method-whenempty) method. The static `unwrap` method returns the collection's underlying items from the given value when applicable: - Collection::unwrap(collect('John Doe')); - - // ['John Doe'] +```php +Collection::unwrap(collect('John Doe')); - Collection::unwrap(['John Doe']); +// ['John Doe'] - // ['John Doe'] +Collection::unwrap(['John Doe']); - Collection::unwrap('John Doe'); +// ['John Doe'] - // 'John Doe' +Collection::unwrap('John Doe'); +// 'John Doe' +``` #### `values()` {.collection-method} The `values` method returns a new collection with the keys reset to consecutive integers: - $collection = new Collection([ - 10 => ['product' => 'Desk', 'price' => 200], - 11 => ['product' => 'Desk', 'price' => 200] - ]); +```php +$collection = new Collection([ + 10 => ['product' => 'Desk', 'price' => 200], + 11 => ['product' => 'Desk', 'price' => 200] +]); - $values = $collection->values(); +$values = $collection->values(); - $values->all(); +$values->all(); - /* - [ - 0 => ['product' => 'Desk', 'price' => 200], - 1 => ['product' => 'Desk', 'price' => 200], - ] - */ +/* + [ + 0 => ['product' => 'Desk', 'price' => 200], + 1 => ['product' => 'Desk', 'price' => 200], + ] +*/ +``` #### `when()` {#collection-method} The `when` method will execute the given callback when the first argument given to the method evaluates to `true`: - $collection = collect([1, 2, 3]); +```php +$collection = collect([1, 2, 3]); - $collection->when(true, function ($collection) { - return $collection->push(4); - }); +$collection->when(true, function ($collection) { + return $collection->push(4); +}); - $collection->when(false, function ($collection) { - return $collection->push(5); - }); +$collection->when(false, function ($collection) { + return $collection->push(5); +}); - $collection->all(); +$collection->all(); - // [1, 2, 3, 4] +// [1, 2, 3, 4] +``` For the inverse of `when`, see the [`unless`](#method-unless) method. @@ -2064,39 +2319,41 @@ For the inverse of `when`, see the [`unless`](#method-unless) method. The `whenEmpty` method will execute the given callback when the collection is empty: - $collection = collect(['michael', 'tom']); +```php +$collection = collect(['michael', 'tom']); - $collection->whenEmpty(function ($collection) { - return $collection->push('adam'); - }); +$collection->whenEmpty(function ($collection) { + return $collection->push('adam'); +}); - $collection->all(); +$collection->all(); - // ['michael', 'tom'] +// ['michael', 'tom'] - $collection = collect(); +$collection = collect(); - $collection->whenEmpty(function ($collection) { - return $collection->push('adam'); - }); +$collection->whenEmpty(function ($collection) { + return $collection->push('adam'); +}); - $collection->all(); +$collection->all(); - // ['adam'] +// ['adam'] - $collection = collect(['michael', 'tom']); +$collection = collect(['michael', 'tom']); - $collection->whenEmpty(function ($collection) { - return $collection->push('adam'); - }, function ($collection) { - return $collection->push('taylor'); - }); +$collection->whenEmpty(function ($collection) { + return $collection->push('adam'); +}, function ($collection) { + return $collection->push('taylor'); +}); - $collection->all(); +$collection->all(); - // ['michael', 'tom', 'taylor'] +// ['michael', 'tom', 'taylor'] +``` For the inverse of `whenEmpty`, see the [`whenNotEmpty`](#method-whennotempty) method. @@ -2105,39 +2362,41 @@ For the inverse of `whenEmpty`, see the [`whenNotEmpty`](#method-whennotempty) m The `whenNotEmpty` method will execute the given callback when the collection is not empty: - $collection = collect(['michael', 'tom']); +```php +$collection = collect(['michael', 'tom']); - $collection->whenNotEmpty(function ($collection) { - return $collection->push('adam'); - }); +$collection->whenNotEmpty(function ($collection) { + return $collection->push('adam'); +}); - $collection->all(); +$collection->all(); - // ['michael', 'tom', 'adam'] +// ['michael', 'tom', 'adam'] - $collection = collect(); +$collection = collect(); - $collection->whenNotEmpty(function ($collection) { - return $collection->push('adam'); - }); +$collection->whenNotEmpty(function ($collection) { + return $collection->push('adam'); +}); - $collection->all(); +$collection->all(); - // [] +// [] - $collection = collect(); +$collection = collect(); - $collection->whenNotEmpty(function ($collection) { - return $collection->push('adam'); - }, function ($collection) { - return $collection->push('taylor'); - }); +$collection->whenNotEmpty(function ($collection) { + return $collection->push('adam'); +}, function ($collection) { + return $collection->push('taylor'); +}); - $collection->all(); +$collection->all(); - // ['taylor'] +// ['taylor'] +``` For the inverse of `whenNotEmpty`, see the [`whenEmpty`](#method-whenempty) method. @@ -2146,44 +2405,48 @@ For the inverse of `whenNotEmpty`, see the [`whenEmpty`](#method-whenempty) meth The `where` method filters the collection by a given key / value pair: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 100], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Door', 'price' => 100], +]); - $filtered = $collection->where('price', 100); +$filtered = $collection->where('price', 100); - $filtered->all(); +$filtered->all(); - /* - [ - ['product' => 'Chair', 'price' => 100], - ['product' => 'Door', 'price' => 100], - ] - */ +/* + [ + ['product' => 'Chair', 'price' => 100], + ['product' => 'Door', 'price' => 100], + ] +*/ +``` The `where` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereStrict`](#method-wherestrict) method to filter using "strict" comparisons. Optionally, you may pass a comparison operator as the second parameter. - $collection = collect([ - ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], - ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], - ['name' => 'Sue', 'deleted_at' => null], - ]); +```php +$collection = collect([ + ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], + ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], + ['name' => 'Sue', 'deleted_at' => null], +]); - $filtered = $collection->where('deleted_at', '!=', null); +$filtered = $collection->where('deleted_at', '!=', null); - $filtered->all(); +$filtered->all(); - /* - [ - ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], - ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], - ] - */ +/* + [ + ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], + ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], + ] +*/ +``` #### `whereStrict()` {#collection-method} @@ -2195,48 +2458,52 @@ This method has the same signature as the [`where`](#method-where) method; howev The `whereBetween` method filters the collection within a given range: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 80], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Pencil', 'price' => 30], - ['product' => 'Door', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 80], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Pencil', 'price' => 30], + ['product' => 'Door', 'price' => 100], +]); - $filtered = $collection->whereBetween('price', [100, 200]); +$filtered = $collection->whereBetween('price', [100, 200]); - $filtered->all(); +$filtered->all(); - /* - [ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], - ] - */ +/* + [ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Door', 'price' => 100], + ] +*/ +``` #### `whereIn()` {#collection-method} The `whereIn` method filters the collection by a given key / value contained within the given array: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 100], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Door', 'price' => 100], +]); - $filtered = $collection->whereIn('price', [150, 200]); +$filtered = $collection->whereIn('price', [150, 200]); - $filtered->all(); +$filtered->all(); - /* - [ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Bookcase', 'price' => 150], - ] - */ +/* + [ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Bookcase', 'price' => 150], + ] +*/ +``` The `whereIn` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereInStrict`](#method-whereinstrict) method to filter using "strict" comparisons. @@ -2250,67 +2517,73 @@ This method has the same signature as the [`whereIn`](#method-wherein) method; h The `whereInstanceOf` method filters the collection by a given class type: - use App\User; - use App\Post; +```php +use App\User; +use App\Post; - $collection = collect([ - new User, - new User, - new Post, - ]); +$collection = collect([ + new User, + new User, + new Post, +]); - $filtered = $collection->whereInstanceOf(User::class); +$filtered = $collection->whereInstanceOf(User::class); - $filtered->all(); +$filtered->all(); - // [App\User, App\User] +// [App\User, App\User] +``` #### `whereNotBetween()` {#collection-method} The `whereNotBetween` method filters the collection within a given range: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 80], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Pencil', 'price' => 30], - ['product' => 'Door', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 80], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Pencil', 'price' => 30], + ['product' => 'Door', 'price' => 100], +]); - $filtered = $collection->whereNotBetween('price', [100, 200]); +$filtered = $collection->whereNotBetween('price', [100, 200]); - $filtered->all(); +$filtered->all(); - /* - [ - ['product' => 'Chair', 'price' => 80], - ['product' => 'Pencil', 'price' => 30], - ] - */ +/* + [ + ['product' => 'Chair', 'price' => 80], + ['product' => 'Pencil', 'price' => 30], + ] +*/ +``` #### `whereNotIn()` {#collection-method} The `whereNotIn` method filters the collection by a given key / value not contained within the given array: - $collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], - ]); +```php +$collection = collect([ + ['product' => 'Desk', 'price' => 200], + ['product' => 'Chair', 'price' => 100], + ['product' => 'Bookcase', 'price' => 150], + ['product' => 'Door', 'price' => 100], +]); - $filtered = $collection->whereNotIn('price', [150, 200]); +$filtered = $collection->whereNotIn('price', [150, 200]); - $filtered->all(); +$filtered->all(); - /* - [ - ['product' => 'Chair', 'price' => 100], - ['product' => 'Door', 'price' => 100], - ] - */ +/* + [ + ['product' => 'Chair', 'price' => 100], + ['product' => 'Door', 'price' => 100], + ] +*/ +``` The `whereNotIn` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereNotInStrict`](#method-wherenotinstrict) method to filter using "strict" comparisons. @@ -2324,80 +2597,87 @@ This method has the same signature as the [`whereNotIn`](#method-wherenotin) met The `whereNotNull` method filters items where the given key is not null: - $collection = collect([ - ['name' => 'Desk'], - ['name' => null], - ['name' => 'Bookcase'], - ]); +```php +$collection = collect([ + ['name' => 'Desk'], + ['name' => null], + ['name' => 'Bookcase'], +]); - $filtered = $collection->whereNotNull('name'); +$filtered = $collection->whereNotNull('name'); - $filtered->all(); +$filtered->all(); - /* - [ - ['name' => 'Desk'], - ['name' => 'Bookcase'], - ] - */ +/* + [ + ['name' => 'Desk'], + ['name' => 'Bookcase'], + ] +*/ +``` #### `whereNull()` {#collection-method} The `whereNull` method filters items where the given key is null: - $collection = collect([ - ['name' => 'Desk'], - ['name' => null], - ['name' => 'Bookcase'], - ]); - - $filtered = $collection->whereNull('name'); +```php +$collection = collect([ + ['name' => 'Desk'], + ['name' => null], + ['name' => 'Bookcase'], +]); - $filtered->all(); +$filtered = $collection->whereNull('name'); - /* - [ - ['name' => null], - ] - */ +$filtered->all(); +/* + [ + ['name' => null], + ] +*/ +``` #### `wrap()` {#collection-method} The static `wrap` method wraps the given value in a collection when applicable: - $collection = Collection::wrap('John Doe'); +```php +$collection = Collection::wrap('John Doe'); - $collection->all(); +$collection->all(); - // ['John Doe'] +// ['John Doe'] - $collection = Collection::wrap(['John Doe']); +$collection = Collection::wrap(['John Doe']); - $collection->all(); +$collection->all(); - // ['John Doe'] +// ['John Doe'] - $collection = Collection::wrap(collect('John Doe')); +$collection = Collection::wrap(collect('John Doe')); - $collection->all(); +$collection->all(); - // ['John Doe'] +// ['John Doe'] +``` #### `zip()` {#collection-method} The `zip` method merges together the values of the given array with the values of the original collection at the corresponding index: - $collection = collect(['Chair', 'Desk']); +```php +$collection = collect(['Chair', 'Desk']); - $zipped = $collection->zip([100, 200]); +$zipped = $collection->zip([100, 200]); - $zipped->all(); +$zipped->all(); - // [['Chair', 100], ['Desk', 200]] +// [['Chair', 100], ['Desk', 200]] +``` ## Higher Order Messages @@ -2406,15 +2686,19 @@ Collections also provide support for "higher order messages", which are short-cu Each higher order message can be accessed as a dynamic property on a collection instance. For instance, let's use the `each` higher order message to call a method on each object within a collection: - $users = User::where('votes', '>', 500)->get(); +```php +$users = User::where('votes', '>', 500)->get(); - $users->each->markAsVip(); +$users->each->markAsVip(); +``` Likewise, we can use the `sum` higher order message to gather the total number of "votes" for a collection of users: - $users = User::where('group', 'Development')->get(); +```php +$users = User::where('group', 'Development')->get(); - return $users->sum->votes; +return $users->sum->votes; +``` ## Lazy Collections @@ -2428,51 +2712,59 @@ To supplement the already powerful `Collection` class, the `LazyCollection` clas For example, imagine your application needs to process a multi-gigabyte log file while taking advantage of Winter's collection methods to parse the logs. Instead of reading the entire file into memory at once, lazy collections may be used to keep only a small part of the file in memory at a given time: - use App\LogEntry; - use Illuminate\Support\LazyCollection; +```php +use App\LogEntry; +use Illuminate\Support\LazyCollection; - LazyCollection::make(function () { - $handle = fopen('log.txt', 'r'); +LazyCollection::make(function () { + $handle = fopen('log.txt', 'r'); - while (($line = fgets($handle)) !== false) { - yield $line; - } - })->chunk(4)->map(function ($lines) { - return LogEntry::fromLines($lines); - })->each(function (LogEntry $logEntry) { - // Process the log entry... - }); + while (($line = fgets($handle)) !== false) { + yield $line; + } +})->chunk(4)->map(function ($lines) { + return LogEntry::fromLines($lines); +})->each(function (LogEntry $logEntry) { + // Process the log entry... +}); +``` Or, imagine you need to iterate through 10,000 Eloquent models. When using traditional Winter collections, all 10,000 Eloquent models must be loaded into memory at the same time: - $users = App\User::all()->filter(function ($user) { - return $user->id > 500; - }); +```php +$users = App\User::all()->filter(function ($user) { + return $user->id > 500; +}); +``` However, the query builder's `cursor` method returns a `LazyCollection` instance. This allows you to still only run a single query against the database but also only keep one Eloquent model loaded in memory at a time. In this example, the `filter` callback is not executed until we actually iterate over each user individually, allowing for a drastic reduction in memory usage: - $users = App\User::cursor()->filter(function ($user) { - return $user->id > 500; - }); +```php +$users = App\User::cursor()->filter(function ($user) { + return $user->id > 500; +}); - foreach ($users as $user) { - echo $user->id; - } +foreach ($users as $user) { + echo $user->id; +} +``` ### Creating Lazy Collections To create a lazy collection instance, you should pass a PHP generator function to the collection's `make` method: - use Illuminate\Support\LazyCollection; +```php +use Illuminate\Support\LazyCollection; - LazyCollection::make(function () { - $handle = fopen('log.txt', 'r'); +LazyCollection::make(function () { + $handle = fopen('log.txt', 'r'); - while (($line = fgets($handle)) !== false) { - yield $line; - } - }); + while (($line = fgets($handle)) !== false) { + yield $line; + } +}); +``` ### The Enumerable Contract @@ -2603,33 +2895,35 @@ In addition to the methods defined in the `Enumerable` contract, the `LazyCollec While the `each` method calls the given callback for each item in the collection right away, the `tapEach` method only calls the given callback as the items are being pulled out of the list one by one: - $lazyCollection = LazyCollection::times(INF)->tapEach(function ($value) { - dump($value); - }); +```php +$lazyCollection = LazyCollection::times(INF)->tapEach(function ($value) { + dump($value); +}); - // Nothing has been dumped so far... +// Nothing has been dumped so far... - $array = $lazyCollection->take(3)->all(); +$array = $lazyCollection->take(3)->all(); - // 1 - // 2 - // 3 +// 1 +// 2 +// 3 +``` #### `remember()` {#collection-method} The `remember` method returns a new lazy collection that will remember any values that have already been enumerated and will not retrieve them again when the collection is enumerated again: - $users = User::cursor()->remember(); - - // No query has been executed yet... - - $users->take(5)->all(); +```php +$users = User::cursor()->remember(); - // The query has been executed and the first 5 users have been hydrated from the database... +// No query has been executed yet... - $users->take(20)->all(); +$users->take(5)->all(); - // First 5 users come from the collection's cache... The rest are hydrated from the database... +// The query has been executed and the first 5 users have been hydrated from the database... +$users->take(20)->all(); +// First 5 users come from the collection's cache... The rest are hydrated from the database... +``` diff --git a/services-config-writer.md b/services-config-writer.md new file mode 100644 index 00000000..7d9a0a5c --- /dev/null +++ b/services-config-writer.md @@ -0,0 +1,300 @@ +# Config Writer + +- [Creating & updating a PHP config file](#create-and-update-php) + - [Setting properties](#setting-properties) + - [Multidimensional arrays](#multidimensional-arrays) + - [Setting env defaults](#setting-env-defaults) + - [Setting function calls](#setting-function-calls) + - [Setting constants](#setting-const) + - [Returning rendered PHP](#returning-rendered-php) + - [Config sorting](#config-sorting) + - [Writing to a different file than read](#writing-to-a-different-file) +- [Creating & updating a .env file](#create-and-update-env) + - [Setting properties](#setting-properties-env) + - [Writing to a different file than read](#writing-to-a-different-file-env) + - [Adding new lines](#adding-new-lines) + +> Notice, the `ConfigWriter` class has been deprecated and replaced by `ConfigFile` + + +## Creating & updating a PHP config file + +The `ConfigFile` class can be used to update & create a php config file. The `ConfigFile::read()` method will take the +path of an existing file or create the file upon save if missing. + +```php +use Winter\Storm\Config\ConfigFile; + +$config = ConfigFile::read('/path/to/file.php'); +$config->set('foo', 'bar'); +$config->write(); +``` + + +### Setting properties + +Setting properties can be chained or multiple properties can be set by passing an array + +```php +ConfigFile::read('/path/to/file.php') + ->set('foo', 'bar') + ->set('bar', 'foo') + ->write(); + +// or + +ConfigFile::read('/path/to/file.php')->set([ + 'foo' => 'bar', + 'bar' => 'foo' +])->write(); +``` + + +### Multidimensional arrays + +Multidimensional arrays can be set via dot notation, or by passing an array. + +```php +ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar.a' => 'bar', + 'foo.bar.b' => 'foo' +])->write(); + +// or + +ConfigFile::read('/path/to/file.php')->set([ + 'foo' => [ + 'bar' => [ + 'a' => 'bar', + 'b' => 'foo' + ] + ] +])->write(); +``` + +Will output: + +```php + [ + 'bar' => [ + 'a' => 'bar', + 'b' => 'foo', + ] + ] +]; +``` + + +### Setting env defaults + +If a config file has a `env()`, then by setting the property of that function will set the default argument. + +For example, the following config: + +```php + [ + 'bar' => env('EXAMPLE_KEY'), + ] +]; +``` + +After setting the position of the env function call. + +```php +ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar' => 'Winter CMS', +])->write(); +``` + +Will result in: + +```php + [ + 'bar' => env('EXAMPLE_KEY', 'Winter CMS'), + ] +]; +``` + + +### Setting function calls + +Function calls can be added to your config either via the `ConfigFunction` class or using the `function()` helper method +on the `ConfigFile` object. + +```php +use Winter\Storm\Config\ConfigFile; +use Winter\Storm\Config\ConfigFunction; + +ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar' => new ConfigFunction('env', ['argument1', 'argument1']), +])->write(); + +// or + +$config = ConfigFile::read('/path/to/file.php'); +$config->set([ + 'foo.bar' => $config->function('env', ['argument1', 'argument1']), +]); +$config->write(); +``` + + +### Setting const + +Constants can be added to your config either via the `ConfigConst` class or using the `const()` helper method +on the `ConfigFile` object. + +```php +use Winter\Storm\Config\ConfigFile; +use Winter\Storm\Config\ConfigConst; + +ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar' => new ConfigConst('PHP_OS'), +])->write(); + +// or + +$config = ConfigFile::read('/path/to/file.php'); +$config->set([ + 'foo.bar' => $config->const('\Path\To\Class::VALUE'), +]); +$config->write(); +``` + + +### Returning rendered PHP + +If you require the php config as a string instead of a file, the `render()` method can be used. + +```php +$phpConfigString = ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar' => 'Winter CMS', +])->render(); +``` + + +### Config sorting + +The `ConfigFile` object supports sorting your config file before rendering. + +```php +$config = ConfigFile::read('/path/to/file.php'); +$config->set([ + 'b' => 'is awesome' + 'a.b' => 'CMS', + 'a.a' => 'Winter', +]); +$config->sort(ConfigFile::SORT_ASC); +$config->write(); +``` + +Will write out: + +```php + [ + 'a' => 'Winter', + 'b' => 'CMS', + ], + 'b' => 'is awesome', +]; +``` + +The sort method supports the following options: + +- `ConfigFile::SORT_ASC` +- `ConfigFile::SORT_DESC` +- a callable function + +By default, `sort()` will use `ConfigFile::SORT_ASC`. + + +### Writing to a different file than read + +If required, you can read from an existing file, and write out to a different file. + +```php +ConfigFile::read('/path/to/file.php')->set([ + 'foo.bar' => 'Winter CMS', +])->write('/path/to/another.file.php'); +``` + + +## Creating & updating a .env file + +By default, the env file read will be `base_path('.env')`, this can be changed if required by passing the path to +the `read()` method. + +> NOTE: properties are set in order, sorting is not supported. + +```php +use Winter\Storm\Config\EnvFile; + +$env = EnvFile::read(); +$env->set('FOO', 'bar'); +$env->write(); +``` + + +### Setting properties + +Similar to the `ConfigFile` object, properties can be set either one at a time or by passing an array. + +> NOTE: dot notation is not supported by `EnvFile` + +```php +$env = EnvFile::read(); +$env->set('FOO', 'bar'); +$env->set('BAR', 'foo'); +$env->write(); + +// or + +EnvFile::read()->set([ + 'FOO' => 'bar' + 'BAR' => 'foo' +])->write(); +``` + + +### Writing to a different file than read + +If required, you can read from an existing file, and write out to a different file. + +```php +EnvFile::read()->set([ + 'FOO' => 'bar', +])->write('/path/to/.env.alternative'); +``` + + +### Adding new lines + +If required, you can add new lines into the env file. + +```php +$env = EnvFile::read(); +$env->set('FOO', 'bar'); +$env->addNewLine(); +$env->set('BAR', 'foo'); +$env->write(); +``` + +Will output: + +```dotenv +FOO="bar" + +BAR="foo" +``` \ No newline at end of file diff --git a/services-error-log.md b/services-error-log.md index d6c15083..51f8067e 100644 --- a/services-error-log.md +++ b/services-error-log.md @@ -34,24 +34,28 @@ The amount of error detail your application displays through the browser is cont For local development, you should set the `debug` value to `true`. In your production environment, this value should always be `false`. - /* - |-------------------------------------------------------------------------- - | Application Debug Mode - |-------------------------------------------------------------------------- - | - | When your application is in debug mode, detailed error messages with - | stack traces will be shown on every error that occurs within your - | application. If disabled, a simple generic error page is shown. - | - */ - - 'debug' => false, +```php +/* +|-------------------------------------------------------------------------- +| Application Debug Mode +|-------------------------------------------------------------------------- +| +| When your application is in debug mode, detailed error messages with +| stack traces will be shown on every error that occurs within your +| application. If disabled, a simple generic error page is shown. +| +*/ + +'debug' => false, +``` #### Log file modes Winter supports `single`, `daily`, `syslog` and `errorlog` logging modes. For example, if you wish to use daily log files instead of a single file, you should simply set the `log` value in your `config/app.php` configuration file: - 'log' => 'daily' +```php +'log' => 'daily' +``` ## Available exceptions @@ -63,7 +67,9 @@ Winter comes with several basic exception types out of the box. The `Winter\Storm\Exception\ApplicationException` class, aliased as `ApplicationException`, is the most common exception type that is used when a simple application condition has failed. - throw new ApplicationException('You must be logged in to do that!'); +```php +throw new ApplicationException('You must be logged in to do that!'); +``` The error message will be simplified and will never include any sensitive information like the php file and line number. @@ -72,7 +78,9 @@ The error message will be simplified and will never include any sensitive inform The `Winter\Storm\Exception\SystemException` class, aliased as `SystemException`, is used for errors that are critical to the system functioning and are always logged. - throw new SystemException('Unable to contact the mail server API'); +```php +throw new SystemException('Unable to contact the mail server API'); +``` When this exception is thrown a detailed error message is shown with the file and line number where it occurred. @@ -81,15 +89,19 @@ When this exception is thrown a detailed error message is shown with the file an The `Winter\Storm\Exception\ValidationException` class, aliased as `ValidationException`, is used for errors that relate directly to a form submission and an invalid field. The message should contain an array with fields and error messages. - throw new ValidationException(['username' => 'Sorry that username is already taken!']); +```php +throw new ValidationException(['username' => 'Sorry that username is already taken!']); +``` You can also pass an instance of the [validation service](validation). - $validation = Validator::make(...); +```php +$validation = Validator::make(...); - if ($validation->fails()) { - throw new ValidationException($validation); - } +if ($validation->fails()) { + throw new ValidationException($validation); +} +``` When this exception is thrown the [AJAX framework](../ajax/introduction) will provide this information in a usable format and focus the first invalid field. @@ -98,7 +110,9 @@ When this exception is thrown the [AJAX framework](../ajax/introduction) will pr The `Winter\Storm\Exception\AjaxException` class, aliased as `AjaxException`, is considered a "smart error" and will return the HTTP code 406. This allows them to pass response contents as if they were a successful response. - throw new AjaxException(['#flashMessages' => $this->renderPartial(...)]); +```php +throw new AjaxException(['#flashMessages' => $this->renderPartial(...)]); +``` When this exception is thrown the [AJAX framework](../ajax/introduction) will follow the standard error workflow but will also refresh specified partials. @@ -109,21 +123,27 @@ All exceptions are handled by the `Winter\Storm\Foundation\Exception\Handler` cl However, you may specify custom handlers if needed using the `App::error` method. Handlers are called based on the type-hint of the Exception they handle. For example, you may create a handler that only handles `RuntimeException` instances: - App::error(function(RuntimeException $exception) { - // Handle the exception... - }); +```php +App::error(function(RuntimeException $exception) { + // Handle the exception... +}); +``` If an exception handler returns a response, that response will be sent to the browser and no other error handlers will be called: - App::error(function(InvalidUserException $exception) { - return 'Sorry! Something is wrong with this account!'; - }); +```php +App::error(function(InvalidUserException $exception) { + return 'Sorry! Something is wrong with this account!'; +}); +``` To listen for PHP fatal errors, you may use the `App::fatal` method: - App::fatal(function($exception) { - // - }); +```php +App::fatal(function($exception) { + // +}); +``` If you have several exception handlers, they should be defined from most generic to most specific. So, for example, a handler that handles all exceptions of type `Exception` should be defined before a custom exception type such as `SystemException`. @@ -136,11 +156,15 @@ Error handler registrations, like [event handlers](events), generally fall under Some exceptions describe HTTP error codes from the server. For example, this may be a "page not found" error (404), an "unauthorized error" (401) or even a developer generated 500 error. In order to generate such a response from anywhere in your application, use the following: - App::abort(404); +```php +App::abort(404); +``` The `abort` method will immediately raise an exception which will be rendered by the exception handler. Optionally, you may provide the response text: - App::abort(403, 'Unauthorized action.'); +```php +App::abort(403, 'Unauthorized action.'); +``` This method may be used at any time during the request's lifecycle. @@ -154,51 +178,61 @@ By default any errors will be shown with a detailed error page containing the fi By default Winter is configured to create a single log file for your application which is stored in the `storage/logs` directory. You may write information to the logs using the `Log` facade: - $user = User::find(1); - Log::info('Showing user profile for user: '.$user->name); +```php +$user = User::find(1); +Log::info('Showing user profile for user: '.$user->name); +``` -The logger provides the eight logging levels defined in [RFC 5424](http://tools.ietf.org/html/rfc5424): **emergency**, **alert**, **critical**, **error**, **warning**, **notice**, **info** and **debug**. +The logger provides the eight logging levels defined in [RFC 5424](https://tools.ietf.org/html/rfc5424): `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info` and `debug`. - Log::emergency($error); - Log::alert($error); - Log::critical($error); - Log::error($error); - Log::warning($error); - Log::notice($error); - Log::info($error); - Log::debug($error); +```php +Log::emergency($error); +Log::alert($error); +Log::critical($error); +Log::error($error); +Log::warning($error); +Log::notice($error); +Log::info($error); +Log::debug($error); +``` #### Contextual information An array of contextual data may also be passed to the log methods. This contextual data will be formatted and displayed with the log message: - Log::info('User failed to login.', ['id' => $user->id]); +```php +Log::info('User failed to login.', ['id' => $user->id]); +``` ### Helper functions There are some global helper methods available to make logging easier. The `trace_log` function is an alias for `Log::info` with support for using arrays and exceptions as the message. - // Write a string value - $val = 'Hello world'; - trace_log('The value is '.$val); +```php +// Write a string value +$val = 'Hello world'; +trace_log('The value is '.$val); - // Dump an array value - $val = ['Some', 'array', 'data']; - trace_log($val); +// Dump an array value +$val = ['Some', 'array', 'data']; +trace_log($val); - // Trace an exception - try { - // - } - catch (Exception $ex) { - trace_log($ex); - } +// Trace an exception +try { + // +} +catch (Exception $ex) { + trace_log($ex); +} +``` The `trace_sql` function enables database logging, when called it will log every command sent to the database. These records only appear in the `system.log` file and will not appear in the administration area log as this is stored in the database and would result in a feedback loop. - trace_sql(); +```php +trace_sql(); - Db::table('users')->count(); +Db::table('users')->count(); - // select count(*) as aggregate from users +// select count(*) as aggregate from users +``` diff --git a/services-filesystem-cdn.md b/services-filesystem-cdn.md index 201da2c3..5293c3df 100644 --- a/services-filesystem-cdn.md +++ b/services-filesystem-cdn.md @@ -24,6 +24,7 @@ Of course, you may configure as many disks as you like, and may even have multip #### The local driver When using the `local` driver, note that all file operations are relative to the `root` directory defined in your configuration file. By default, this value is set to the `storage/app` directory. Therefore, the following method would store a file in `storage/app/file.txt`: + ```php Storage::disk('local')->put('file.txt', 'Contents'); ``` @@ -38,6 +39,7 @@ Before using the S3 or Rackspace drivers, you will need to install [Drivers plug ### Obtaining disk instances The `Storage` facade may be used to interact with any of your configured disks. For example, you may use the `put` method on the facade to store an avatar on the default disk. If you call methods on the `Storage` facade without first calling the `disk` method, the method call will automatically be passed to the default disk: + ```php $user = User::find($id); @@ -47,95 +49,122 @@ Storage::put( ); ``` When using multiple disks, you may access a particular disk using the `disk` method on the `Storage` facade. Of course, you may continue to chain methods to execute methods on the disk: + ```php $disk = Storage::disk('s3'); $contents = Storage::disk('local')->get('file.jpg') ``` + ### Retrieving files The `get` method may be used to retrieve the contents of a given file. The raw string contents of the file will be returned by the method: + ```php $contents = Storage::get('file.jpg'); ``` + The `exists` method may be used to determine if a given file exists on the disk: + ```php $exists = Storage::disk('s3')->exists('file.jpg'); ``` + #### File meta information The `size` method may be used to get the size of the file in bytes: + ```php $size = Storage::size('file1.jpg'); ``` + The `lastModified` method returns the UNIX timestamp of the last time the file was modified: + ```php $time = Storage::lastModified('file1.jpg'); ``` + ### Storing files The `put` method may be used to store a file on disk. You may also pass a PHP `resource` to the `put` method, which will use Flysystem's underlying stream support. Using streams is greatly recommended when dealing with large files: + ```php Storage::put('file.jpg', $contents); Storage::put('file.jpg', $resource); ``` + The `copy` method may be used to copy an existing file to a new location on the disk: + ```php Storage::copy('old/file1.jpg', 'new/file1.jpg'); ``` + The `move` method may be used to move an existing file to a new location: + ```php Storage::move('old/file1.jpg', 'new/file1.jpg'); ``` + #### Prepending / appending to files The `prepend` and `append` methods allow you to easily insert content at the beginning or end of a file: + ```php Storage::prepend('file.log', 'Prepended Text'); Storage::append('file.log', 'Appended Text'); ``` + ### Deleting files The `delete` method accepts a single filename or an array of files to remove from the disk: + ```php Storage::delete('file.jpg'); Storage::delete(['file1.jpg', 'file2.jpg']); ``` + ### Directories #### Get all files within a directory The `files` method returns an array of all of the files in a given directory. If you would like to retrieve a list of all files within a given directory including all sub-directories, you may use the `allFiles` method: + ```php $files = Storage::files($directory); $files = Storage::allFiles($directory); ``` + #### Get all directories within a directory The `directories` method returns an array of all the directories within a given directory. Additionally, you may use the `allDirectories` method to get a list of all directories within a given directory and all of its sub-directories: + ```php $directories = Storage::directories($directory); // Recursive... $directories = Storage::allDirectories($directory); ``` + #### Create a directory The `makeDirectory` method will create the given directory, including any needed sub-directories: + ```php Storage::makeDirectory($directory); ``` + #### Delete a directory Finally, the `deleteDirectory` may be used to remove a directory, including all of its files, from the disk: + ```php Storage::deleteDirectory($directory); ``` diff --git a/services-hashing-encryption.md b/services-hashing-encryption.md index 947feb10..ebd9fc19 100644 --- a/services-hashing-encryption.md +++ b/services-hashing-encryption.md @@ -16,9 +16,11 @@ The `Hash` facade provides secure Bcrypt hashing for storing user passwords. Bcr You may hash a password by calling the `make` method on the `Hash` facade: - $user = new User; - $user->password = Hash::make('mypassword'); - $user->save(); +```php +$user = new User; +$user->password = Hash::make('mypassword'); +$user->save(); +``` Alternatively, models can implement the [Hashable trait](../database/traits#hashable) to automatically hash attributes. @@ -26,17 +28,21 @@ Alternatively, models can implement the [Hashable trait](../database/traits#hash The `check` method allows you to verify that a given plain-text string corresponds to a given hash. - if (Hash::check('plain-text', $hashedPassword)) { - // The passwords match... - } +```php +if (Hash::check('plain-text', $hashedPassword)) { + // The passwords match... +} +``` #### Checking if a password needs to be rehashed The `needsRehash` function allows you to determine if the work factor used by the hasher has changed since the password was hashed: - if (Hash::needsRehash($hashed)) { - $hashed = Hash::make('plain-text'); - } +```php +if (Hash::needsRehash($hashed)) { + $hashed = Hash::make('plain-text'); +} +``` ## Encryption @@ -45,19 +51,23 @@ You may encrypt a value using the `Crypt` facade. All encrypted values are encry For example, we may use the `encrypt` method to encrypt a secret and store it on a [database model](../database/model): - $user = new User; - $user->secret = Crypt::encrypt('shhh no telling'); - $user->save(); +```php +$user = new User; +$user->secret = Crypt::encrypt('shhh no telling'); +$user->save(); +``` #### Decrypting a value Of course, you may decrypt values using the `decrypt` method on the `Crypt` facade. If the value can not be properly decrypted, such as when the MAC is invalid, an `Illuminate\Contracts\Encryption\DecryptException` exception will be thrown: - use Illuminate\Contracts\Encryption\DecryptException; +```php +use Illuminate\Contracts\Encryption\DecryptException; - try { - $decrypted = Crypt::decrypt($encryptedValue); - } - catch (DecryptException $ex) { - // - } +try { + $decrypted = Crypt::decrypt($encryptedValue); +} +catch (DecryptException $ex) { + // +} +``` diff --git a/services-helpers.md b/services-helpers.md index ab3c08bd..b4c63f00 100644 --- a/services-helpers.md +++ b/services-helpers.md @@ -16,6 +16,7 @@ Winter includes a variety of "helper" PHP functions. Many of these functions are ### Arrays
    +[Laravel `Arr::*()` Helpers](https://laravel.com/docs/6.x/helpers#available-methods) [array_add](#method-array-add) [array_divide](#method-array-divide) [array_dot](#method-array-dot) @@ -56,6 +57,7 @@ Winter includes a variety of "helper" PHP functions. Many of these functions are ### Strings
    +[Laravel `Str::*()` Helpers](https://laravel.com/docs/6.x/helpers#available-methods) [camel_case](#method-camel-case) [class_basename](#method-class-basename) [e](#method-e) @@ -113,250 +115,289 @@ Winter includes a variety of "helper" PHP functions. Many of these functions are The `array_add` function adds a given key / value pair to the array if the given key doesn't already exist in the array: - $array = array_add(['name' => 'Desk'], 'price', 100); +```php +$array = array_add(['name' => 'Desk'], 'price', 100); - // ['name' => 'Desk', 'price' => 100] +// ['name' => 'Desk', 'price' => 100] +``` #### `array_divide()` {#collection-method} The `array_divide` function returns two arrays, one containing the keys, and the other containing the values of the original array: - list($keys, $values) = array_divide(['name' => 'Desk']); +```php +list($keys, $values) = array_divide(['name' => 'Desk']); - // $keys: ['name'] +// $keys: ['name'] - // $values: ['Desk'] +// $values: ['Desk'] +``` #### `array_dot()` {#collection-method} The `array_dot` function flattens a multi-dimensional array into a single level array that uses "dot" notation to indicate depth: - $array = array_dot(['foo' => ['bar' => 'baz']]); +```php +$array = array_dot(['foo' => ['bar' => 'baz']]); - // ['foo.bar' => 'baz']; +// ['foo.bar' => 'baz']; +``` #### `array_undot()` {#collection-method} The `array_undot` function is the counter-part to the `array_dot` method. It will convert a dot-notated array into a standard associative array: - $array = array_undot([ - 'foo.bar' => 'baz' - ]); - - // [ - // 'foo' => [ - // 'bar' => 'baz' - // ] - // ] +```php +$array = array_undot([ + 'foo.bar' => 'baz' +]); +// [ +// 'foo' => [ +// 'bar' => 'baz' +// ] +// ] +``` #### `array_except()` {#collection-method} The `array_except` method removes the given key / value pairs from the array: - $array = ['name' => 'Desk', 'price' => 100]; +```php +$array = ['name' => 'Desk', 'price' => 100]; - $array = array_except($array, ['price']); +$array = array_except($array, ['price']); - // ['name' => 'Desk'] +// ['name' => 'Desk'] +``` #### `array_first()` {#collection-method} The `array_first` method returns the first element of an array passing a given truth test: - $array = [100, 200, 300]; +```php +$array = [100, 200, 300]; - $value = array_first($array, function ($key, $value) { - return $value >= 150; - }); +$value = array_first($array, function ($key, $value) { + return $value >= 150; +}); - // 200 +// 200 +``` A default value may also be passed as the third parameter to the method. This value will be returned if no value passes the truth test: - $value = array_first($array, $callback, $default); +```php +$value = array_first($array, $callback, $default); +``` #### `array_flatten()` {#collection-method} The `array_flatten` method will flatten a multi-dimensional array into a single level. - $array = ['name' => 'Joe', 'languages' => ['PHP', 'Ruby']]; +```php +$array = ['name' => 'Joe', 'languages' => ['PHP', 'Ruby']]; - $array = array_flatten($array); +$array = array_flatten($array); - // ['Joe', 'PHP', 'Ruby']; +// ['Joe', 'PHP', 'Ruby']; +``` #### `array_forget()` {#collection-method} The `array_forget` method removes a given key / value pair from a deeply nested array using "dot" notation: - $array = ['products' => ['desk' => ['price' => 100]]]; +```php +$array = ['products' => ['desk' => ['price' => 100]]]; - array_forget($array, 'products.desk'); +array_forget($array, 'products.desk'); - // ['products' => []] +// ['products' => []] +``` #### `array_get()` {#collection-method} The `array_get` method retrieves a value from a deeply nested array using "dot" notation: - $array = ['products' => ['desk' => ['price' => 100]]]; +```php +$array = ['products' => ['desk' => ['price' => 100]]]; - $value = array_get($array, 'products.desk'); +$value = array_get($array, 'products.desk'); - // ['price' => 100] +// ['price' => 100] +``` The `array_get` function also accepts a default value, which will be returned if the specific key is not found: - $value = array_get($array, 'names.john', 'default'); +```php +$value = array_get($array, 'names.john', 'default'); +``` #### `array_only()` {#collection-method} The `array_only` method will return only the specified key / value pairs from the given array: - $array = ['name' => 'Desk', 'price' => 100, 'orders' => 10]; +```php +$array = ['name' => 'Desk', 'price' => 100, 'orders' => 10]; - $array = array_only($array, ['name', 'price']); +$array = array_only($array, ['name', 'price']); - // ['name' => 'Desk', 'price' => 100] +// ['name' => 'Desk', 'price' => 100] +``` #### `array_pluck()` {#collection-method} The `array_pluck` method will pluck a list of the given key / value pairs from the array: - $array = [ - ['developer' => ['name' => 'Brian']], - ['developer' => ['name' => 'Stewie']] - ]; +```php +$array = [ + ['developer' => ['name' => 'Brian']], + ['developer' => ['name' => 'Stewie']] +]; - $array = array_pluck($array, 'developer.name'); +$array = array_pluck($array, 'developer.name'); - // ['Brian', 'Stewie']; +// ['Brian', 'Stewie']; +``` #### `array_pull()` {#collection-method} The `array_pull` method returns and removes a key / value pair from the array: - $array = ['name' => 'Desk', 'price' => 100]; +```php +$array = ['name' => 'Desk', 'price' => 100]; - $name = array_pull($array, 'name'); +$name = array_pull($array, 'name'); - // $name: Desk +// $name: Desk - // $array: ['price' => 100] +// $array: ['price' => 100] +``` #### `array_set()` {#collection-method} The `array_set` method sets a value within a deeply nested array using "dot" notation: - $array = ['products' => ['desk' => ['price' => 100]]]; +```php +$array = ['products' => ['desk' => ['price' => 100]]]; - array_set($array, 'products.desk.price', 200); +array_set($array, 'products.desk.price', 200); - // ['products' => ['desk' => ['price' => 200]]] +// ['products' => ['desk' => ['price' => 200]]] +``` #### `array_sort()` {#collection-method} The `array_sort` method sorts the array by the results of the given Closure: - $array = [ - ['name' => 'Desk'], - ['name' => 'Chair'], - ]; +```php +$array = [ + ['name' => 'Desk'], + ['name' => 'Chair'], +]; - $array = array_values(array_sort($array, function ($value) { - return $value['name']; - })); +$array = array_values(array_sort($array, function ($value) { + return $value['name']; +})); - /* - [ - ['name' => 'Chair'], - ['name' => 'Desk'], - ] - */ +/* + [ + ['name' => 'Chair'], + ['name' => 'Desk'], + ] +*/ +``` #### `array_sort_recursive()` {#collection-method} The `array_sort_recursive` function recursively sorts the array using the `sort` function: - $array = [ +```php +$array = [ + [ + 'Brian', + 'Shannon', + 'Alec', + ], + [ + 'PHP', + 'Ruby', + 'JavaScript', + ], +]; + +$array = array_sort_recursive($array); + +/* + [ [ + 'Alec', 'Brian', 'Shannon', - 'Alec', ], [ + 'JavaScript', 'PHP', 'Ruby', - 'JavaScript', - ], + ] ]; - - $array = array_sort_recursive($array); - - /* - [ - [ - 'Alec', - 'Brian', - 'Shannon', - ], - [ - 'JavaScript', - 'PHP', - 'Ruby', - ] - ]; - */ +*/ +``` #### `array_where()` {#collection-method} The `array_where` function filters the array using the given Closure: - $array = [100, '200', 300, '400', 500]; +```php +$array = [100, '200', 300, '400', 500]; - $array = array_where($array, function ($value, $key) { - return is_string($value); - }); +$array = array_where($array, function ($value, $key) { + return is_string($value); +}); - // [1 => 200, 3 => 400] +// [1 => 200, 3 => 400] +``` #### `head()` {#collection-method} The `head` function simply returns the first element in the given array: - $array = [100, 200, 300]; +```php +$array = [100, 200, 300]; - $first = head($array); +$first = head($array); - // 100 +// 100 +``` #### `last()` {#collection-method} The `last` function returns the last element in the given array: - $array = [100, 200, 300]; +```php +$array = [100, 200, 300]; - $last = last($array); +$last = last($array); - // 300 +// 300 +``` ## Paths @@ -366,7 +407,9 @@ The `last` function returns the last element in the given array: Path prefix symbols can be used to create a dynamic path. For example, a path beginning with `~/` will create a path relative to the application: - list: ~/plugins/acme/pay/models/invoiceitem/columns.yaml +```yaml +list: ~/plugins/acme/pay/models/invoiceitem/columns.yaml +``` These symbols are supported for creating dynamic paths: @@ -380,113 +423,153 @@ Symbol | Description The `app_path` function returns the fully qualified path to the `app` directory: - $path = app_path(); +```php +$path = app_path(); +``` You may also use the `app_path` function to generate a fully qualified path to a given file relative to the application directory: - $path = app_path('Http/Controllers/Controller.php'); +```php +$path = app_path('Http/Controllers/Controller.php'); +``` #### `base_path()` {#collection-method} The `base_path` function returns the fully qualified path to the project root: - $path = base_path(); +```php +$path = base_path(); +``` You may also use the `base_path` function to generate a fully qualified path to a given file relative to the application directory: - $path = base_path('vendor/bin'); +```php +$path = base_path('vendor/bin'); +``` #### `config_path($path = '')` {#collection-method} The `config_path` function returns the fully qualified path to the application configuration directory: - $path = config_path(); +```php +$path = config_path(); +``` You may also use the `config_path` function to generate a fully qualified path to a given file relative to the config directory: - $path = config_path('dev/cms.php'); +```php +$path = config_path('dev/cms.php'); +``` #### `database_path()` {#collection-method} The `database_path` function returns the fully qualified path to the application's database directory: - $path = database_path(); +```php +$path = database_path(); +``` #### `media_path($path = '')` {#collection-method} The `media_path` function returns the fully qualified path to the application media directory: - $path = media_path(); +```php +$path = media_path(); +``` You may also use the `media_path` function to generate a fully qualified path to a given file relative to the media directory: - $path = media_path('images/myimage.png'); +```php +$path = media_path('images/myimage.png'); +``` #### `plugins_path($path = '')` {#collection-method} The `plugins_path` function returns the fully qualified path to the application plugin directory: - $path = plugins_path(); +```php +$path = plugins_path(); +``` You may also use the `plugins_path` function to generate a fully qualified path to a given file relative to the plugins directory: - $path = plugins_path('author/plugin/routes.php'); +```php +$path = plugins_path('author/plugin/routes.php'); +``` #### `public_path()` {#collection-method} The `public_path` function returns the fully qualified path to the `public` directory: - $path = public_path(); +```php +$path = public_path(); +``` #### `storage_path($path = '')` {#collection-method} The `storage_path` function returns the fully qualified path to the `storage` directory: - $path = storage_path(); +```php +$path = storage_path(); +``` You may also use the `storage_path` function to generate a fully qualified path to a given file relative to the storage directory: - $path = storage_path('app/file.txt'); +```php +$path = storage_path('app/file.txt'); +``` #### `temp_path($path = '')` {#collection-method} The `temp_path` function returns the fully qualified path to a writable directory for temporary files: - $path = temp_path(); +```php +$path = temp_path(); +``` You may also use the `temp_path` function to generate a fully qualified path to a given file relative to the temp directory: - $path = temp_path('app/file.txt'); +```php +$path = temp_path('app/file.txt'); +``` #### `themes_path($path = '')` {#collection-method} The `themes_path` function returns the fully qualified path to the `themes` directory: - $path = themes_path(); +```php +$path = themes_path(); +``` You may also use the `themes_path` function to generate a fully qualified path to a given file relative to the themes directory: - $path = themes_path('mytheme/file.txt'); +```php +$path = themes_path('mytheme/file.txt'); +``` #### `uploads_path($path = '')` {#collection-method} The `uploads_path` function returns the fully qualified path to the application uploads directory: - $path = uploads_path(); +```php +$path = uploads_path(); +``` You may also use the `uploads_path` function to generate a fully qualified path to a given file relative to the uploads directory: - $path = uploads_path('public/file.txt'); +```php +$path = uploads_path('public/file.txt'); +``` ## Strings @@ -496,155 +579,189 @@ You may also use the `uploads_path` function to generate a fully qualified path The `camel_case` function converts the given string to `camelCase`: - $camel = camel_case('foo_bar'); +```php +$camel = camel_case('foo_bar'); - // fooBar +// fooBar +``` #### `class_basename()` {#collection-method} The `class_basename` returns the class name of the given class with the class' namespace removed: - $class = class_basename('Foo\Bar\Baz'); +```php +$class = class_basename('Foo\Bar\Baz'); - // Baz +// Baz +``` #### `e()` {#collection-method} The `e` function runs `htmlentities` over the given string: - echo e('foo'); +```php +echo e('foo'); - // <html>foo</html> +// <html>foo</html> +``` #### `ends_with()` {#collection-method} The `ends_with` function determines if the given string ends with the given value: - $value = ends_with('This is my name', 'name'); +```php +$value = ends_with('This is my name', 'name'); - // true +// true +``` #### `snake_case()` {#collection-method} The `snake_case` function converts the given string to `snake_case`: - $snake = snake_case('fooBar'); +```php +$snake = snake_case('fooBar'); - // foo_bar +// foo_bar +``` #### `str_limit()` {#collection-method} The `str_limit` function limits the number of characters in a string. The function accepts a string as its first argument and the maximum number of resulting characters as its second argument: - $value = str_limit('The CMS platform that gets back to basics.', 6); +```php +$value = str_limit('The CMS platform that gets back to basics.', 6); - // The CMS... +// The CMS... +``` #### `starts_with()` {#collection-method} The `starts_with` function determines if the given string begins with the given value: - $value = starts_with('The cow goes moo', 'The'); +```php +$value = starts_with('The cow goes moo', 'The'); - // true +// true +``` #### `str_contains()` {#collection-method} The `str_contains` function determines if the given string contains the given value: - $value = str_contains('The bird goes tweet', 'bird'); +```php +$value = str_contains('The bird goes tweet', 'bird'); - // true +// true +``` #### `str_finish()` {#collection-method} The `str_finish` function adds a single instance of the given value to a string: - $string = str_finish('this/string', '/'); +```php +$string = str_finish('this/string', '/'); - // this/string/ +// this/string/ +``` #### `str_is()` {#collection-method} The `str_is` function determines if a given string matches a given pattern. Asterisks may be used to indicate wildcards: - $value = str_is('foo*', 'foobar'); +```php +$value = str_is('foo*', 'foobar'); - // true +// true - $value = str_is('baz*', 'foobar'); +$value = str_is('baz*', 'foobar'); - // false +// false +``` #### `str_plural()` {#collection-method} The `str_plural` function converts a string to its plural form. This function currently only supports the English language: - $plural = str_plural('car'); +```php +$plural = str_plural('car'); - // cars +// cars - $plural = str_plural('child'); +$plural = str_plural('child'); - // children +// children +``` #### `str_random()` {#collection-method} The `str_random` function generates a random string of the specified length: - $string = str_random(40); +```php +$string = str_random(40); +``` #### `str_singular()` {#collection-method} The `str_singular` function converts a string to its singular form. This function currently only supports the English language: - $singular = str_singular('cars'); +```php +$singular = str_singular('cars'); - // car +// car +``` #### `str_slug()` {#collection-method} The `str_slug` function generates a URL friendly "slug" from the given string: - $title = str_slug("Winter CMS", "-"); +```php +$title = str_slug("Winter CMS", "-"); - // winter-cms +// winter-cms +``` #### `studly_case()` {#collection-method} The `studly_case` function converts the given string to `StudlyCase`: - $value = studly_case('foo_bar'); +```php +$value = studly_case('foo_bar'); - // FooBar +// FooBar +``` #### `trans()` {#collection-method} The `trans` function translates the given language line using your [localization files](../plugin/localization): - echo trans('validation.required'): +```php +echo trans('validation.required'): +``` #### `trans_choice()` {#collection-method} The `trans_choice` function translates the given language line with inflection: - $value = trans_choice('foo.bar', $count); +```php +$value = trans_choice('foo.bar', $count); +``` ## Miscellaneous @@ -654,135 +771,181 @@ The `trans_choice` function translates the given language line with inflection: Generate a URL for an asset using the current scheme of the request (HTTP or HTTPS): - $url = asset('img/photo.jpg'); +```php +$url = asset('img/photo.jpg'); +``` + +You can configure the asset URL host by setting the `ASSET_URL` variable in your `.env` file (or `asset_url` in your `config/app.php` file). This can be useful if you host your assets on an external service like Amazon S3 or another CDN: + +```php +// ASSET_URL=http://example.com/assets + +$url = asset('img/photo.jpg'); // http://example.com/assets/img/photo.jpg +``` #### `config()` {#collection-method} The `config` function gets the value of a configuration variable. The configuration values may be accessed using "dot" syntax, which includes the name of the file and the option you wish to access. A default value may be specified and is returned if the configuration option does not exist: - $value = config('app.timezone'); +```php +$value = config('app.timezone'); - $value = config('app.timezone', $default); +$value = config('app.timezone', $default); +``` The `config` helper may also be used to set configuration variables at runtime by passing an array of key / value pairs: - config(['app.debug' => true]); +```php +config(['app.debug' => true]); +``` #### `dd()` {#collection-method} The `dd` function dumps the given variable and ends execution of the script: - dd($value); +```php +dd($value); +``` #### `env()` {#collection-method} The `env` function gets the value of an environment variable or returns a default value: - $env = env('APP_ENV'); +```php +$env = env('APP_ENV'); - // Return a default value if the variable doesn't exist... - $env = env('APP_ENV', 'production'); +// Return a default value if the variable doesn't exist... +$env = env('APP_ENV', 'production'); +``` #### `get()` {#collection-method} The `get` function obtains an input item from the request, restricted to GET variables only: - $value = get('key', $default = null) +```php +$value = get('key', $default = null) +``` #### `input()` {#collection-method} The `input` function obtains an input item from the request: - $value = input('key', $default = null) +```php +$value = input('key', $default = null) +``` #### `post()` {#collection-method} The `post` function obtains an input item from the request, restricted to POST variables only: - $value = post('key', $default = null) +```php +$value = post('key', $default = null) +``` #### `redirect()` {#collection-method} The `redirect` function return an instance of the redirector to do [redirect responses](../services/response-view#redirects): - return redirect('/home'); +```php +return redirect('/home'); +``` #### `request()` {#collection-method} The `request` function returns the current [request instance](../services/request-input): - $referer = request()->header('referer'); +```php +$referer = request()->header('referer'); +``` #### `response()` {#collection-method} The `response` function creates a [response](../services/response-view) instance or obtains an instance of the response factory: - return response('Hello World', 200, $headers); +```php +return response('Hello World', 200, $headers); - return response()->json(['foo' => 'bar'], 200, $headers); +return response()->json(['foo' => 'bar'], 200, $headers); +``` #### `route()` {#collection-method} The `route` function generates a URL for the given [named route](../services/router): - $url = route('routeName'); +```php +$url = route('routeName'); +``` If the route accepts parameters, you may pass them as the second argument to the method: - $url = route('routeName', ['id' => 1]); +```php +$url = route('routeName', ['id' => 1]); +``` #### `secure_asset()` {#collection-method} Generate a URL for an asset using HTTPS: - echo secure_asset('foo/bar.zip', $title, $attributes = []); +```php +echo secure_asset('foo/bar.zip', $title, $attributes = []); +``` #### `trace_log()` {#collection-method} The `trace_log` function writes a trace message to the log file. - trace_log('This code has passed...'); +```php +trace_log('This code has passed...'); +``` The function supports passing exceptions, arrays and objects: - trace_log($exception); +```php +trace_log($exception); - trace_log($array); +trace_log($array); - trace_log($object); +trace_log($object); +``` You may also pass multiple arguments to trace multiple messages: - trace_log($value1, $value2, $exception, '...'); +```php +trace_log($value1, $value2, $exception, '...'); +``` #### `trace_sql()` {#collection-method} The `trace_sql` function enables database logging and begins to monitor all SQL output. - trace_sql(); +```php +trace_sql(); - Db::table('users')->count(); +Db::table('users')->count(); - // select count(*) as aggregate from users +// select count(*) as aggregate from users +``` #### `url()` {#collection-method} The `url` function generates a fully qualified URL to the given path: - echo url('user/profile'); +```php +echo url('user/profile'); - echo url('user/profile', [1]); +echo url('user/profile', [1]); +``` diff --git a/services-html.md b/services-html.md index ddd2864a..688ef6ca 100644 --- a/services-html.md +++ b/services-html.md @@ -18,11 +18,13 @@ Winter provides various helpful functions with the `Html` facade, useful for dealing with HTML and forms. While most of the examples will use the PHP language all of these features translate directly to [Twig markup](../markup) with a simple conversion. - // PHP - +```php +// PHP + - // Twig - {{ form_open(...) }} +// Twig +{{ form_open(...) }} +``` As you can see above, in Twig all functions prefixed with `form_` will bind directly to the `Form` facade and provide access to the methods using *snake_case*. See the [markup guide for more information](../markup/function-form) on using the form helper in the frontend. @@ -31,45 +33,61 @@ As you can see above, in Twig all functions prefixed with `form_` will bind dire Forms can be opened with the `Form::open` method that passes an array of attributes as the first argument: - 'foo/bar']) ?> - // - +```php + 'foo/bar']) ?> + // + +``` By default, a `POST` method will be assumed, however, you are free to specify another method: - Form::open(['url' => 'foo/bar', 'method' => 'put']) +```php +Form::open(['url' => 'foo/bar', 'method' => 'put']) +``` > **NOTE:** Since HTML forms only support `POST` and `GET`, `PUT` and `DELETE` methods will be spoofed by automatically adding a `_method` hidden field to your form. You may pass in regular HTML attributes as well: - Form::open(['url' => 'foo/bar', 'class' => 'pretty-form']) +```php +Form::open(['url' => 'foo/bar', 'class' => 'pretty-form']) +``` If your form is going to accept file uploads, add a `files` option to your array: - Form::open(['url' => 'foo/bar', 'files' => true]) +```php +Form::open(['url' => 'foo/bar', 'files' => true]) +``` You may also open forms that point to handler methods in your page or components: - Form::open(['request' => 'onSave']) +```php +Form::open(['request' => 'onSave']) +``` #### AJAX enabled forms Likewise, AJAX enabled forms can be opened using the `Form::ajax` method where the first argument is the handler method name: - Form::ajax('onSave') +```php +Form::ajax('onSave') +``` The second argument of `Form::ajax` should contain the attributes: - Form::ajax('onSave', ['confirm' => 'Are you sure?']) +```php +Form::ajax('onSave', ['confirm' => 'Are you sure?']) +``` You can also pass partials to update as another array: - Form::ajax('onSave', ['update' => [ - 'control-panel' => '#controlPanel', - 'layout/sidebar' => '#layoutSidebar' - ] - ]) +```php +Form::ajax('onSave', ['update' => [ + 'control-panel' => '#controlPanel', + 'layout/sidebar' => '#layoutSidebar' + ] +]) +``` > **NOTE**: Most [data attributes from the AJAX framework](../ajax/attributes-api) are available here by dropping the `data-request-` prefix. @@ -80,13 +98,17 @@ You can also pass partials to update as another array: If you have [protection enabled](../setup/configuration#csrf-protection), using the `Form::open` method with `POST`, `PUT` or `DELETE` will automatically add a CSRF token to your forms as a hidden field. Alternatively, if you wish to generate the HTML for the hidden CSRF field, you may use the `token` method: - +```php + +``` #### Deferred binding session key A session key used for [deferred binding](../database/relations#deferred-binding) will be added to every form as a hidden field. If you want to generate this field manually, you may use the `sessionKey` method: - +```php + +``` ## Form model binding @@ -95,7 +117,9 @@ A session key used for [deferred binding](../database/relations#deferred-binding You may want to populate a form based on the contents of a model. To do so, use the `Form::model` method: - 'userForm']) ?> +```php + 'userForm']) ?> +``` Now when you generate a form element, like a text input, the model's value matching the field's name will automatically be set as the field value. So for example, for a text input named `email`, the user model's `email` attribute would be set as the value. If there is an item in the Session flash data matching the input name, that will take precedence over the model's value. The priority looks like this: @@ -106,11 +130,15 @@ Now when you generate a form element, like a text input, the model's value match This allows you to quickly build forms that not only bind to model values, but easily re-populate if there is a validation error on the server. You can manually access these values using `Form::value`: - +```php + +``` You may pass a default value as the second argument: - +```php + +``` > **NOTE:** When using `Form::model`, be sure to close your form with `Form::close`! @@ -119,11 +147,15 @@ You may pass a default value as the second argument: #### Generating a label element - +```php + +``` #### Specifying extra HTML attributes - 'awesome']) ?> +```php + 'awesome']) ?> +``` > **NOTE:** After creating a label, any form element you create with a name matching the label name will automatically receive an ID matching the label name as well. @@ -132,51 +164,67 @@ You may pass a default value as the second argument: #### Generating A Text Input - +```php + +``` #### Specifying a default value - +```php + +``` > **NOTE:** The *hidden* and *textarea* methods have the same signature as the *text* method. #### Generating a password input - +```php + +``` #### Generating other inputs - - +```php + + +``` ## Checkboxes and radio buttons #### Generating a checkbox or radio input - +```php + - + +``` #### Generating a checkbox or radio input that is checked - +```php + - + +``` ## Number #### Generating a number input - +```php + +``` ## File input #### Generating a file input - +```php + +``` > **NOTE:** The form must have been opened with the `files` option set to `true`. @@ -185,41 +233,57 @@ You may pass a default value as the second argument: #### Generating a drop-down list - 'Large', 'S' => 'Small']) ?> +```php + 'Large', 'S' => 'Small']) ?> +``` #### Generating a drop-down list with selected default - 'Large', 'S' => 'Small'], 'S') ?> +```php + 'Large', 'S' => 'Small'], 'S') ?> +``` #### Generating a grouped list - ['leopard' => 'Leopard'], - 'Dogs' => ['spaniel' => 'Spaniel'], - ]) ?> +```php + ['leopard' => 'Leopard'], + 'Dogs' => ['spaniel' => 'Spaniel'], +]) ?> +``` #### Generating a drop-down list with a range - +```php + +``` #### Generating a drop-down list with a range, selected value and blank option - 'Choose...']) ?> +```php + 'Choose...']) ?> +``` #### Generating a list with month names - +```php + +``` #### Generating a list with month names, selected value and blank option - 'Choose month...']) ?> +```php + 'Choose month...']) ?> +``` ## Buttons #### Generating a submit button - +```php + +``` > **NOTE:** Need to create a button element? Try the *button* method. It has the same signature as *submit*. @@ -230,12 +294,16 @@ You may pass a default value as the second argument: It's easy to define your own custom Form class helpers called "macros". Here's how it works. First, simply register the macro with a given name and a Closure: - Form::macro('myField', function() { - return ''; - }) +```php +Form::macro('myField', function() { + return ''; +}) +``` Now you can call your macro using its name: #### Calling A Custom Form Macro - +```php + +``` diff --git a/services-image-resizing.md b/services-image-resizing.md index a9996cea..36aaf47b 100644 --- a/services-image-resizing.md +++ b/services-image-resizing.md @@ -47,6 +47,14 @@ If `$width` or `$height` is falsey or `'auto'`, that value is calculated using o The following elements are supported in the options array are supported: + +
    + Key | Description | Default | Options --- | --- | --- | --- `mode` | How the image should be fitted to dimensions | `auto` | `exact`, `portrait`, `landscape`, `auto`, `fit`, or `crop` @@ -59,6 +67,12 @@ Key | Description | Default | Options The `mode` option allows you to specify how the image should be resized. The available modes are as follows: + +
    + Mode | Description --- | --- `auto` | Automatically choose between `portrait` and `landscape` based on the image's orientation diff --git a/services-mail.md b/services-mail.md index 1e285f60..89f48d3a 100644 --- a/services-mail.md +++ b/services-mail.md @@ -26,48 +26,58 @@ Before using the Mailgun, SparkPost or SES drivers you will need to install [Dri To use the Mailgun driver, set the `driver` option in your `config/mail.php` configuration file to `mailgun`. Next, verify that your `config/services.php` configuration file contains the following options: - 'mailgun' => [ - 'domain' => 'your-mailgun-domain', - 'secret' => 'your-mailgun-key', - 'endpoint' => 'api.mailgun.net', // api.eu.mailgun.net for EU - ], +```php +'mailgun' => [ + 'domain' => 'your-mailgun-domain', + 'secret' => 'your-mailgun-key', + 'endpoint' => 'api.mailgun.net', // api.eu.mailgun.net for EU +], +``` #### SparkPost driver To use the SparkPost driver set the `driver` option in your `config/mail.php` configuration file to `sparkpost`. Next, verify that your `config/services.php` configuration file contains the following options: - 'sparkpost' => [ - 'secret' => 'your-sparkpost-key', - ], +```php +'sparkpost' => [ + 'secret' => 'your-sparkpost-key', +], +``` #### SES driver To use the Amazon SES driver set the `driver` option in your `config/mail.php` configuration file to `ses`. Then, verify that your `config/services.php` configuration file contains the following options: - 'ses' => [ - 'key' => 'your-ses-key', - 'secret' => 'your-ses-secret', - 'region' => 'ses-region', // e.g. us-east-1 - ], +```php +'ses' => [ + 'key' => 'your-ses-key', + 'secret' => 'your-ses-secret', + 'region' => 'ses-region', // e.g. us-east-1 +], +``` ## Sending mail To send a message, use the `send` method on the `Mail` facade which accepts three arguments. The first argument is a unique *mail code* used to locate either the [mail view](#mail-views) or [mail template](#mail-templates). The second argument is an array of data you wish to pass to the view. The third argument is a `Closure` callback which receives a message instance, allowing you to customize the recipients, subject, and other aspects of the mail message: - // These variables are available inside the message as Twig - $vars = ['name' => 'Joe', 'user' => 'Mary']; +```php +// These variables are available inside the message as Twig +$vars = ['name' => 'Joe', 'user' => 'Mary']; - Mail::send('acme.blog::mail.message', $vars, function($message) { +Mail::send('acme.blog::mail.message', $vars, function($message) { - $message->to('admin@domain.tld', 'Admin Person'); - $message->subject('This is a reminder'); + $message->to('admin@domain.tld', 'Admin Person'); + $message->subject('This is a reminder'); - }); +}); +``` Since we are passing an array containing the `name` key in the example above, we could display the value within our e-mail view using the following Twig markup: - {{ name }} +```twig +{{ name }} +``` > **NOTE:** You should avoid passing a `message` variable in your message, this variable is always passed and allows the [inline embedding of attachments](#attachments). @@ -75,30 +85,40 @@ Since we are passing an array containing the `name` key in the example above, we Winter also includes an alternative method called `sendTo` that can simplify sending mail: - // Send to address using no name - Mail::sendTo('admin@domain.tld', 'acme.blog::mail.message', $params); +```php +// Send to address using no name +Mail::sendTo('admin@domain.tld', 'acme.blog::mail.message', $params); - // Send using an object's properties - Mail::sendTo($user, 'acme.blog::mail.message', $params); +// Send using an object's properties +Mail::sendTo($user, 'acme.blog::mail.message', $params); - // Send to multiple addresses - Mail::sendTo(['admin@domain.tld' => 'Admin Person'], 'acme.blog::mail.message', $params); +// Send to multiple addresses +Mail::sendTo(['admin@domain.tld' => 'Admin Person'], 'acme.blog::mail.message', $params); - // Alternatively send a raw message without parameters - Mail::rawTo('admin@domain.tld', 'Hello friend'); +// Alternatively send a raw message without parameters +Mail::rawTo('admin@domain.tld', 'Hello friend'); +``` The first argument in `sendTo` is used for the recipients can take different value types: + +
    + Type | Description ------------- | ------------- -String | a single recipient address, with no name defined. -Array | multiple recipients where the array key is the address and the value is the name. -Object | a single recipient object, where the *email* property is used for the address and the *name* is optionally used for the name. -Collection | a collection of recipient objects, as above. +`String` | a single recipient address, with no name defined. +`Array` | multiple recipients where the array key is the address and the value is the name. +`Object` | a single recipient object, where the *email* property is used for the address and the *name* is optionally used for the name. +`Collection` | a collection of recipient objects, as above. The complete signature of `sendTo` is as follows: - Mail::sendTo($recipient, $message, $params, $callback, $options); +```php +Mail::sendTo($recipient, $message, $params, $callback, $options); +``` - `$recipient` is defined as above. - `$message` is the template name or message contents for raw sending. @@ -115,30 +135,34 @@ The following custom sending `$options` are supported As previously mentioned, the third argument given to the `send` method is a `Closure` allowing you to specify various options on the e-mail message itself. Using this Closure you may specify other attributes of the message, such as carbon copies, blind carbon copies, etc: - Mail::send('acme.blog::mail.welcome', $vars, function($message) { +```php +Mail::send('acme.blog::mail.welcome', $vars, function($message) { - $message->from('us@example.com', 'Winter'); - $message->to('foo@example.com')->cc('bar@example.com'); + $message->from('us@example.com', 'Winter'); + $message->to('foo@example.com')->cc('bar@example.com'); - }); +}); +``` Here is a list of the available methods on the `$message` message builder instance: - $message->from($address, $name = null); - $message->sender($address, $name = null); - $message->to($address, $name = null); - $message->cc($address, $name = null); - $message->bcc($address, $name = null); - $message->replyTo($address, $name = null); - $message->subject($subject); - $message->priority($level); - $message->attach($pathToFile, array $options = []); +```php +$message->from($address, $name = null); +$message->sender($address, $name = null); +$message->to($address, $name = null); +$message->cc($address, $name = null); +$message->bcc($address, $name = null); +$message->replyTo($address, $name = null); +$message->subject($subject); +$message->priority($level); +$message->attach($pathToFile, array $options = []); - // Attach a file from a raw $data string... - $message->attachData($data, $name, array $options = []); +// Attach a file from a raw $data string... +$message->attachData($data, $name, array $options = []); - // Get the underlying SwiftMailer message instance... - $message->getSwiftMessage(); +// Get the underlying SwiftMailer message instance... +$message->getSwiftMessage(); +``` > **NOTE:** The message instance passed to a `Mail::send` Closure extends the [SwiftMailer](http://swiftmailer.org) message class, allowing you to call any method on that class to build your e-mail messages. @@ -146,51 +170,65 @@ Here is a list of the available methods on the `$message` message builder instan By default, the view given to the `send` method is assumed to contain HTML. However, by passing an array as the first argument to the `send` method, you may specify a plain text view to send in addition to the HTML view: - Mail::send(['acme.blog::mail.html', 'acme.blog::mail.text'], $data, $callback); +```php +Mail::send(['acme.blog::mail.html', 'acme.blog::mail.text'], $data, $callback); +``` Or, if you only need to send a plain text e-mail, you may specify this using the `text` key in the array: - Mail::send(['text' => 'acme.blog::mail.text'], $data, $callback); +```php +Mail::send(['text' => 'acme.blog::mail.text'], $data, $callback); +``` #### Mailing parsed raw strings You may use the `raw` method if you wish to e-mail a raw string directly. This content will be parsed by Markdown. - Mail::raw('Text to e-mail', function ($message) { - // - }); +```php +Mail::raw('Text to e-mail', function ($message) { + // +}); +``` Additionally this string will be parsed by Twig, if you wish to pass variables to this environment, use the `send` method instead, passing the content as the `raw` key. - Mail::send(['raw' => 'Text to email'], $vars, function ($message) { - // - }); +```php +Mail::send(['raw' => 'Text to email'], $vars, function ($message) { + // +}); +``` #### Mailing raw strings If you pass an array containing either `text` or `html` keys, this will be an explicit request to send mail. No layout or markdown parsing is used. - Mail::raw([ - 'text' => 'This is plain text', - 'html' => 'This is HTML' - ], function ($message) { - // - }); +```php +Mail::raw([ + 'text' => 'This is plain text', + 'html' => 'This is HTML' +], function ($message) { + // +}); +``` ### Attachments To add attachments to an e-mail, use the `attach` method on the `$message` object passed to your Closure. The `attach` method accepts the full path to the file as its first argument: - Mail::send('acme.blog::mail.welcome', $data, function ($message) { - // +```php +Mail::send('acme.blog::mail.welcome', $data, function ($message) { + // - $message->attach($pathToFile); - }); + $message->attach($pathToFile); +}); +``` When attaching files to a message, you may also specify the display name and / or MIME type by passing an `array` as the second argument to the `attach` method: - $message->attach($pathToFile, ['as' => $display, 'mime' => $mime]); +```php +$message->attach($pathToFile, ['as' => $display, 'mime' => $mime]); +``` ### Inline attachments @@ -199,29 +237,35 @@ When attaching files to a message, you may also specify the display name and / o Embedding inline images into your e-mails is typically cumbersome; however, there is a convenient way to attach images to your e-mails and retrieving the appropriate CID. To embed an inline image, use the `embed` method on the `message` variable within your e-mail view. Remember, the `message` variable is available to all of your mail views: - - Here is an image: +```twig + + Here is an image: - - + + +``` If you are planning to use queued emails make sure that the path of the file is absolute. To achieve that you can simply use the [app filter](../markup/filter-app): - - Here is an image: - {% set pathToFile = 'storage/app/media/path/to/file.jpg' | app %} - - +```twig + + Here is an image: + {% set pathToFile = 'storage/app/media/path/to/file.jpg' | app %} + + +``` #### Embedding raw data in mail content If you already have a raw data string you wish to embed into an e-mail message, you may use the `embedData` method on the `message` variable: - - Here is an image from raw data: +```twig + + Here is an image from raw data: - - + + +``` ### Queueing mail @@ -230,9 +274,11 @@ If you already have a raw data string you wish to embed into an e-mail message, Since sending mail messages can drastically lengthen the response time of your application, many developers choose to queue messages for background sending. This is easy using the built-in [unified queue API](../services/queues). To queue a mail message, use the `queue` method on the `Mail` facade: - Mail::queue('acme.blog::mail.welcome', $data, function ($message) { - // - }); +```php +Mail::queue('acme.blog::mail.welcome', $data, function ($message) { + // +}); +``` This method will automatically take care of pushing a job onto the queue to send the mail message in the background. Of course, you will need to [configure your queues](../services/queues) before using this feature. @@ -240,21 +286,25 @@ This method will automatically take care of pushing a job onto the queue to send If you wish to delay the delivery of a queued e-mail message, you may use the `later` method. To get started, simply pass the number of seconds by which you wish to delay the sending of the message as the first argument to the method: - Mail::later(5, 'acme.blog::mail.welcome', $data, function ($message) { - // - }); +```php +Mail::later(5, 'acme.blog::mail.welcome', $data, function ($message) { + // +}); +``` #### Pushing to specific queues If you wish to specify a specific queue on which to push the message, you may do so using the `queueOn` and `laterOn` methods: - Mail::queueOn('queue-name', 'acme.blog::mail.welcome', $data, function ($message) { - // - }); +```php +Mail::queueOn('queue-name', 'acme.blog::mail.welcome', $data, function ($message) { + // +}); - Mail::laterOn('queue-name', 5, 'acme.blog::mail.welcome', $data, function ($message) { - // - }); +Mail::laterOn('queue-name', 5, 'acme.blog::mail.welcome', $data, function ($message) { + // +}); +``` ## Message content @@ -268,43 +318,49 @@ Optionally, mail views can be [registered in the Plugin registration file](#mail Mail views reside in the file system and the code used represents the path to the view file. For example sending mail with the code **author.plugin::mail.message** would use the content in following file: - plugins/ <=== Plugins directory - author/ <=== "author" segment - plugin/ <=== "plugin" segment - views/ <=== View directory - mail/ <=== "mail" segment - message.htm <=== "message" segment +```css +πŸ“‚ plugins <=== Plugins directory +β”— πŸ“‚ author <=== "author" segment + β”— πŸ“‚ plugin <=== "plugin" segment + β”— πŸ“‚ views <=== View directory + β”— πŸ“‚ mail <=== "mail" segment + β”— πŸ“œ message.htm <=== "message" segment +``` The content inside a mail view file can include up to 3 sections: **configuration**, **plain text**, and **HTML markup**. Sections are separated with the `==` sequence. For example: - subject = "Your product has been added to Winter CMS project" - == +```twig +subject = "Your product has been added to Winter CMS project" +== - Hi {{ name }}, +Hi {{ name }}, - Good news! User {{ user }} just added your product "{{ product }}" to a project. +Good news! User {{ user }} just added your product "{{ product }}" to a project. - This message was sent using no formatting (plain text) - == +This message was sent using no formatting (plain text) +== -

    Hi {{ name }},

    +

    Hi {{ name }},

    -

    Good news! User {{ user }} just added your product {{ product }} to a project.

    +

    Good news! User {{ user }} just added your product {{ product }} to a project.

    -

    This email was sent using formatting (HTML)

    +

    This email was sent using formatting (HTML)

    +``` > **NOTE:** Basic Twig tags and expressions are supported in mail views. The **plain text** section is optional and a view can contain only the configuration and HTML markup sections. - subject = "Your product has been added to Winter CMS project" - == +```twig +subject = "Your product has been added to Winter CMS project" +== -

    Hi {{ name }},

    +

    Hi {{ name }},

    -

    This email does not support plain text.

    +

    This email does not support plain text.

    -

    Sorry about that!

    +

    Sorry about that!

    +``` #### Configuration section @@ -312,8 +368,8 @@ The configuration section sets the mail view parameters. The following configura Parameter | Description ------------- | ------------- -**subject** | the mail message subject, required. -**layout** | the [mail layout](#mail-layouts) code, optional. Default value is `default`. +`subject` | the mail message subject, required. +`layout` | the [mail layout](#mail-layouts) code, optional. Default value is `default`. ### Using mail templates @@ -322,10 +378,12 @@ Mail templates reside in the database and can be created in the backend area via The process for sending these emails is the same. For example, if you create a template with code *this.is.my.email* you can send it using this PHP code: - Mail::send('this.is.my.email', $data, function($message) use ($user) - { - [...] - }); +```php +Mail::send('this.is.my.email', $data, function($message) use ($user) +{ + [...] +}); +``` > **NOTE:** If the mail template does not exist in the system, this code will attempt to find a mail view with the same code. @@ -344,43 +402,45 @@ By default, Winter comes with two primary mail layouts: Layout | Code | Description ------------- | ------------- | ------------- -Default | default | Used for public facing, frontend mail -System | system | Used for internal, backend mail +Default | `default` | Used for public facing, frontend mail +System | `system` | Used for internal, backend mail ### Registering mail layouts, templates & partials Mail views can be registered as templates that are automatically generated in the backend ready for customization. Mail templates can be customized via the *Settings > Mail templates* menu. The templates can be registered by overriding the `registerMailTemplates` method of the [Plugin registration class](../plugin/registration#registration-file). - public function registerMailTemplates() - { - return [ - 'winter.user::mail.activate', - 'winter.user::mail.restore' - ]; - } +```php +public function registerMailTemplates() +{ + return [ + 'winter.user::mail.activate', + 'winter.user::mail.restore' + ]; +} +``` The method should return an array of [mail view names](#mail-views). Like templates, mail partials and layouts can be registered by overriding the `registerMailPartials` and `registerMailLayouts` methods of the [Plugin registration class](../plugin/registration#registration-file). - - public function registerMailPartials() - { - return [ - 'tracking' => 'acme.blog::partials.tracking', - 'promotion' => 'acme.blog::partials.promotion', - ]; - } - - public function registerMailLayouts() - { - return [ - 'marketing' => 'acme.blog::layouts.marketing', - 'notification' => 'acme.blog::layouts.notification', - ]; - } - +```php +public function registerMailPartials() +{ + return [ + 'tracking' => 'acme.blog::partials.tracking', + 'promotion' => 'acme.blog::partials.promotion', + ]; +} + +public function registerMailLayouts() +{ + return [ + 'marketing' => 'acme.blog::layouts.marketing', + 'notification' => 'acme.blog::layouts.notification', + ]; +} +``` The methods should return an array of [mail view names](#mail-views). The array key will be used as `code` property for the partial or layout. @@ -389,7 +449,9 @@ The methods should return an array of [mail view names](#mail-views). The array You may register variables that are globally available to all mail templates 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 mail templates. @@ -406,13 +468,17 @@ One solution is to use the `log` mail driver during local development. This driv Another solution is to set a universal recipient of all e-mails sent by the framework. This way, all the emails generated by your application will be sent to a specific address, instead of the address actually specified when sending the message. This can be done via the `to` option in your `config/mail.php` configuration file: - 'to' => [ - 'address' => 'dev@example.com', - 'name' => 'Dev Example' - ], +```php +'to' => [ + 'address' => 'dev@example.com', + 'name' => 'Dev Example' +], +``` #### Pretend mail mode You can dynamically disable sending mail using the `Mail::pretend` method. When the mailer is in pretend mode, messages will be written to your application's log files instead of being sent to the recipient. - Mail::pretend(); +```php +Mail::pretend(); +``` diff --git a/services-pagination.md b/services-pagination.md index f3f918d4..aff84d1a 100644 --- a/services-pagination.md +++ b/services-pagination.md @@ -19,7 +19,9 @@ There are several ways to paginate items. The simplest is by using the `paginate First, let's take a look at calling the `paginate` method on a query. In this example, the only argument passed to `paginate` is the number of items you would like displayed "per page". In this case, let's specify that we would like to display `15` items per page: - $users = Db::table('users')->paginate(15); +```php +$users = Db::table('users')->paginate(15); +``` > **NOTE:** Currently, pagination operations that use a `groupBy` statement cannot be executed efficiently. If you need to use a `groupBy` with a paginated result set, it is recommended that you query the database and create a paginator manually. @@ -27,26 +29,36 @@ First, let's take a look at calling the `paginate` method on a query. In this ex If you only need to display simple "Next" and "Previous" links in your pagination view, you have the option of using the `simplePaginate` method to perform a more efficient query. This is very useful for large datasets if you do not need to display a link for each page number when rendering your view: - $users = Db::table('users')->simplePaginate(15); +```php +$users = Db::table('users')->simplePaginate(15); +``` ### Paginating model results You may also paginate [database model](../database/model) queries. In this example, we will paginate the `User` model with `15` items per page. As you can see, the syntax is nearly identical to paginating query builder results: - $users = User::paginate(15); +```php +$users = User::paginate(15); +``` Of course, you may call `paginate` after setting other constraints on the query, such as `where` clauses: - $users = User::where('votes', '>', 100)->paginate(15); +```php +$users = User::where('votes', '>', 100)->paginate(15); +``` You may also use the `simplePaginate` method when paginating models: - $users = User::where('votes', '>', 100)->simplePaginate(15); +```php +$users = User::where('votes', '>', 100)->simplePaginate(15); +``` You may specify the page number manually by passing a second argument, here we paginate `15` items per page, specifying that we are on page `2`: - $users = User::where('votes', '>', 100)->paginate(15, 2); +```php +$users = User::where('votes', '>', 100)->paginate(15, 2); +``` ### Manually creating a paginator @@ -66,13 +78,15 @@ When you call the `paginate` or `simplePaginate` methods on a query builder or m So once you have retrieved the results, you may display the results and render the page links using Twig: -
    - {% for user in users %} - {{ user.name }} - {% endfor %} -
    +```twig +
    + {% for user in users %} + {{ user.name }} + {% endfor %} +
    - {{ users.render|raw }} +{{ users.render|raw }} +``` The `render` method will render the links to the rest of the pages in the result set. Each of these links will already contain the proper `?page` query string variable. The HTML generated by the `render` method is compatible with the [Bootstrap CSS framework](https://getbootstrap.com). @@ -80,63 +94,75 @@ The `render` method will render the links to the rest of the pages in the result #### Customizing the paginator URI -The `setPath` method allows you to customize the URI used by the paginator when generating links. For example, if you want the paginator to generate links like `http://example.com/custom/url?page=N`, you should pass `custom/url` to the `setPath` method: +The `setPath` method allows you to customize the URI used by the paginator when generating links. For example, if you want the paginator to generate links like `https://example.com/custom/url?page=N`, you should pass `custom/url` to the `setPath` method: - $users = User::paginate(15); - $users->setPath('custom/url'); +```php +$users = User::paginate(15); +$users->setPath('custom/url'); +``` #### Appending to pagination links You may add to the query string of pagination links using the `appends` method. For example, to append `&sort=votes` to each pagination link, you should make the following call to `appends`: - {{ users.appends({sort: 'votes'}).render|raw }} +```twig +{{ users.appends({sort: 'votes'}).render|raw }} +``` If you wish to append a "hash fragment" to the paginator's URLs, you may use the `fragment` method. For example, to append `#foo` to the end of each pagination link, make the following call to the `fragment` method: - {{ users.fragment('foo').render|raw }} +```twig +{{ users.fragment('foo').render|raw }} +``` #### Additional helper methods You may also access additional pagination information via the following methods on paginator instances: - $results->count() - $results->currentPage() - $results->hasMorePages() - $results->lastPage() (Not available when using simplePaginate) - $results->nextPageUrl() - $results->perPage() - $results->previousPageUrl() - $results->total() (Not available when using simplePaginate) - $results->url($page) +```php +$results->count() +$results->currentPage() +$results->hasMorePages() +$results->lastPage() // Not available when using simplePaginate +$results->nextPageUrl() +$results->perPage() +$results->previousPageUrl() +$results->total() // Not available when using simplePaginate +$results->url($page) +``` ## Converting results to JSON The paginator result classes implement the `Illuminate\Contracts\Support\JsonableInterface` contract and expose the `toJson` method, so it's very easy to convert your pagination results to JSON. You may also convert a paginator instance to JSON by simply returning it from a route or AJAX handler: - Route::get('users', function () { - return User::paginate(); - }); +```php +Route::get('users', function () { + return User::paginate(); +}); +``` The JSON from the paginator will include meta information such as `total`, `current_page`, `last_page`, and more. The actual result objects will be available via the `data` key in the JSON array. Here is an example of the JSON created by returning a paginator instance from a route: #### Example Paginator JSON - { - "total": 50, - "per_page": 15, - "current_page": 1, - "last_page": 4, - "next_page_url": "http://wintercms.app?page=2", - "prev_page_url": null, - "from": 1, - "to": 15, - "data":[ - { - // Result Object - }, - { - // Result Object - } - ] - } +```json +{ + "total": 50, + "per_page": 15, + "current_page": 1, + "last_page": 4, + "next_page_url": "https://wintercms.app?page=2", + "prev_page_url": null, + "from": 1, + "to": 15, + "data": [ + { + // Result Object + }, + { + // Result Object + } + ] +} +``` diff --git a/services-parser.md b/services-parser.md index c1354991..48ecdf0b 100644 --- a/services-parser.md +++ b/services-parser.md @@ -22,20 +22,26 @@ Winter uses several standards for processing markup, templates and configuration Markdown allows you to write easy-to-read and easy-to-write plain text format, which then converts to HTML. The `Markdown` facade is used for parsing Markdown syntax and is based on [GitHub flavored markdown](https://help.github.com/articles/github-flavored-markdown/). Some quick examples of markdown: - This text is **bold**, this text is *italic*, this text is ~~crossed out~~. +```md +This text is **bold**, this text is *italic*, this text is ~~crossed out~~. - # The largest heading (an

    tag) - ## The second largest heading (an

    tag) - ... - ###### The 6th largest heading (an

    tag) +# The largest heading (an

    tag) +## The second largest heading (an

    tag) +... +###### The 6th largest heading (an

    tag) +``` Use the `Markdown::parse` method to render Markdown to HTML: - $html = Markdown::parse($markdown); +```php +$html = Markdown::parse($markdown); +``` You may also use the `|md` filter for [parsing Markdown in your frontend markup](../markup/filter-md). - {{ '**Text** is bold.'|md }} +```twig +{{ '**Text** is bold.' | md }} +``` ## Twig template parser @@ -44,11 +50,15 @@ Twig is a simple but powerful template engine that parses HTML templates in to o The `Twig` facade is used for parsing Twig syntax, you may use the `Twig::parse` method to render Twig to HTML. - $html = Twig::parse($twig); +```php +$html = Twig::parse($twig); +``` The second argument can be used for passing variables to the Twig markup. - $html = Twig::parse($twig, ['foo' => 'bar']); +```php +$html = Twig::parse($twig, ['foo' => 'bar']); +``` The Twig parser can be extended to register custom features via [the plugin registration file](../plugin/registration#extending-twig). @@ -57,9 +67,11 @@ The Twig parser can be extended to register custom features via [the plugin regi Winter also ships with a simple bracket template parser as an alternative to the Twig parser, currently used for passing variables to [theme content blocks](../cms/content#content-variables). This engine is faster to render HTML and is designed to be more suitable for non-technical users. There is no facade for this parser so the fully qualified `Winter\Storm\Parse\Bracket` class should be used with the `parse` method. - use Winter\Storm\Parse\Bracket; +```php +use Winter\Storm\Parse\Bracket; - $html = Bracket::parse($content, ['foo' => 'bar']); +$html = Bracket::parse($content, ['foo' => 'bar']); +``` The syntax uses singular *curly brackets* for rendering variables: @@ -67,88 +79,112 @@ The syntax uses singular *curly brackets* for rendering variables: You may also pass an array of objects to parse as a variable. - $html = Template::parse($content, ['likes' => [ - ['name' => 'Dogs'], - ['name' => 'Fishing'], - ['name' => 'Golf'] - ]]); +```php +$html = Template::parse($content, ['likes' => [ + ['name' => 'Dogs'], + ['name' => 'Fishing'], + ['name' => 'Golf'] +]]); +``` The array can be iterated using the following syntax: -
      - {likes} -
    • {name}
    • - {/likes} -
    +```twig +
      + {likes} +
    • {name}
    • + {/likes} +
    +``` ## YAML configuration parser YAML ("YAML Ain't Markup Language") is a configuration format, similar to Markdown it was designed to be an easy-to-read and easy-to-write format that converts to a PHP array. It is used practically everywhere for the backend development of Winter, such as [form field](../backend/forms#form-fields) and [list column](../backend/lists##list-columns) definitions. An example of some YAML: - receipt: Acme Purchase Invoice - date: 2015-10-02 - user: - name: Joe - surname: Blogs +```yaml +receipt: Acme Purchase Invoice +date: 2015-10-02 +user: + name: Joe + surname: Blogs +``` The `Yaml` facade is used for parsing YAML and you use the `Yaml::parse` method to render YAML to a PHP array: - $array = Yaml::parse($yamlString); +```php +$array = Yaml::parse($yamlString); +``` Use the `parseFile` method to parse the contents of a file: - $array = Yaml::parseFile($filePath); +```php +$array = Yaml::parseFile($filePath); +``` The parser also supports operation in reverse, outputting YAML format from a PHP array. You may use the `render` method for this: - $yamlString = Yaml::render($array); +```php +$yamlString = Yaml::render($array); +``` ## Initialization (INI) configuration parser The INI file format is a standard for defining simple configuration files, commonly used by [components inside theme templates](../cms/components). It could be considered a cousin of the YAML format, although unlike YAML, it is incredibly simple, less sensitive to typos and does not rely on indentation. It supports basic key-value pairs with sections, for example: - receipt = "Acme Purchase Invoice" - date = "2015-10-02" +```ini +receipt = "Acme Purchase Invoice" +date = "2015-10-02" - [user] - name = "Joe" - surname = "Blogs" +[user] +name = "Joe" +surname = "Blogs" +``` The `Ini` facade is used for parsing INI and you use the `Ini::parse` method to render INI to a PHP array: - $array = Ini::parse($iniString); +```php +$array = Ini::parse($iniString); +``` Use the `parseFile` method to parse the contents of a file: - $array = Ini::parseFile($filePath); +```php +$array = Ini::parseFile($filePath); +``` The parser also supports operation in reverse, outputting INI format from a PHP array. You may use the `render` method for this: - $iniString = Ini::render($array); +```php +$iniString = Ini::render($array); +``` ### Winter flavored INI Traditionally, the INI parser used by the PHP function `parse_ini_string` is restricted to arrays that are 3 levels deep. For example: - level1Value = "foo" - level1Array[] = "bar" +```ini +level1Value = "foo" +level1Array[] = "bar" - [level1Object] - level2Value = "hello" - level2Array[] = "world" - level2Object[level3Value] = "stop here" +[level1Object] +level2Value = "hello" +level2Array[] = "world" +level2Object[level3Value] = "stop here" +``` Winter has extended this functionality with *Winter flavored INI* to allow arrays of infinite depth, inspired by the syntax of HTML forms. Following on from the above example, the following syntax is supported: - [level1Object] - level2Object[level3Array][] = "Yay!" - level2Object[level3Object][level4Value] = "Yay!" - level2Object[level3Object][level4Array][] = "Yay!" - level2Object[level3Object][level4Object][level5Value] = "Yay!" - ; ... to infinity and beyond! +```ini +[level1Object] +level2Object[level3Array][] = "Yay!" +level2Object[level3Object][level4Value] = "Yay!" +level2Object[level3Object][level4Array][] = "Yay!" +level2Object[level3Object][level4Object][level5Value] = "Yay!" +; ... to infinity and beyond! +``` ## Dynamic Syntax parser @@ -159,27 +195,35 @@ Dynamic Syntax is a templating engine unique to Winter that fundamentally suppor There is no facade for this parser so the fully qualified `Winter\Storm\Parse\Syntax\Parser` class should be used with the `parse` method. The first argument of the `parse` method takes the template content as a string and returns a `Parser` object. - use Winter\Storm\Parse\Syntax\Parser as SyntaxParser; +```php +use Winter\Storm\Parse\Syntax\Parser as SyntaxParser; - $syntax = SyntaxParser::parse($content); +$syntax = SyntaxParser::parse($content); +``` ### View mode Let's say we used the first example above as the template content, calling the `render` method by itself will render the template with the default text: - echo $syntax->render(); - //

    Our wonderful website

    +```php +echo $syntax->render(); +//

    Our wonderful website

    +``` Just like any templating engine, passing an array of variables to the first argument of `render` will replace the variables inside the template. Here the default value of `websiteName` is replaced with our new value: - echo $syntax->render(['websiteName' => 'Winter CMS']); - //

    Winter CMS

    +```php +echo $syntax->render(['websiteName' => 'Winter CMS']); +//

    Winter CMS

    +``` As a bonus feature, calling the `toTwig` method will output the template in a prepared state for rendering by the [Twig engine](#twig-parser). - echo $syntax->toTwig(); - //

    {{ websiteName }}

    +```php +echo $syntax->toTwig(); +//

    {{ websiteName }}

    +``` ### Editor mode @@ -188,20 +232,24 @@ So far the Dynamic Syntax parser is not much different to a regular template eng To continue with the examples above, calling the `toEditor` method on the `Parser` object will return a PHP array of properties that define how the variable should be populated, by a form builder for example. - $array = $syntax->toEditor(); - // 'websiteName' => [ - // 'label' => 'Website name', - // 'default' => 'Our wonderful website', - // 'type' => 'text' - // ] +```php +$array = $syntax->toEditor(); +// 'websiteName' => [ +// 'label' => 'Website name', +// 'default' => 'Our wonderful website', +// 'type' => 'text' +// ] +``` You may notice the properties closely resemble the options found in [form field definitions](../backend/forms#form-fields). This is intentional so the two features compliment each other. We could now easily convert the array above to YAML and write to a `fields.yaml` file: - $form = [ - 'fields' => $syntax->toEditor() - ]; +```php +$form = [ + 'fields' => $syntax->toEditor() +]; - File::put('fields.yaml', Yaml::render($form)); +File::put('fields.yaml', Yaml::render($form)); +``` ### Supported tags @@ -276,7 +324,9 @@ Text input for Markdown content. Renders in Twig as - {{ content|md }} +```twig +{{ content | md }} +```
    @@ -288,7 +338,9 @@ File selector for media library items. This tag value will contain the relative Renders in Twig as - {{ logo|media }} +```twig +{{ logo | media }} +```
    @@ -311,31 +363,35 @@ Renders a repeating section with other fields inside. Renders in Twig as - {% for fields in repeater %} -

    {{ fields.title }}

    -

    {{ fields.content|raw }}

    - {% endfor %} +```twig +{% for fields in repeater %} +

    {{ fields.title }}

    +

    {{ fields.content | raw }}

    +{% endfor %} +``` Calling `$syntax->toEditor` will return a different array for a repeater field: - 'repeater' => [ - 'label' => 'Website name', - 'type' => 'repeater', - 'fields' => [ - - 'title' => [ - 'label' => 'Title', - 'default' => 'Title', - 'type' => 'text' - ], - 'content' => [ - 'label' => 'Content', - 'default' => 'Content', - 'type' => 'textarea' - ] - +```php +'repeater' => [ + 'label' => 'Website name', + 'type' => 'repeater', + 'fields' => [ + + 'title' => [ + 'label' => 'Title', + 'default' => 'Title', + 'type' => 'text' + ], + 'content' => [ + 'label' => 'Content', + 'default' => 'Content', + 'type' => 'textarea' ] + ] +] +``` The repeater field also supports group mode, to be used with the dynamic syntax parser as follows: @@ -344,23 +400,25 @@ The repeater field also supports group mode, to be used with the dynamic syntax This is an example of the repeater_fields.yaml group configuration file: - 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 +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 +``` For more information about the repeater group mode see [Repeater Widget](../backend/forms#widget-repeater). @@ -374,7 +432,9 @@ Text input for rich content (WYSIWYG). Renders in Twig as - {{ content|raw }} +```twig +{{ content | raw }} +```
    diff --git a/services-queues.md b/services-queues.md index de845af6..a2f94c00 100644 --- a/services-queues.md +++ b/services-queues.md @@ -171,11 +171,11 @@ When using Iron.io [push queues](#push-queues), you should take extra precaution ## Running the queue worker -Winter includes some [console commands](../console/commands) that will process jobs in the queue. +Winter includes some [console commands](../console/introduction) that will process jobs in the queue. To process new jobs as they are pushed onto the queue, run the `queue:work` command: -``` +```bash php artisan queue:work ``` @@ -187,7 +187,7 @@ Queue worker processes store the booted application state in memory. They will n To process only the first job on the queue, use the `--once` option: -``` +```bash php artisan queue:work --once ``` @@ -195,13 +195,13 @@ php artisan queue:work --once You may also specify which queue connection the worker should utilize: -``` +```bash php artisan queue:work --once connection ``` You may pass a comma-delimited list of queue connections to the `work` command to set queue priorities: -``` +```bash php artisan queue:work --once --queue=high,low ``` @@ -211,7 +211,7 @@ In this example, jobs on the `high` queue will always be processed before moving You may also set the length of time (in seconds) each job should be allowed to run: -``` +```bash php artisan queue:work --once --timeout=60 ``` @@ -219,7 +219,7 @@ php artisan queue:work --once --timeout=60 In addition, you may specify the number of seconds to wait before polling for new jobs: -``` +```bash php artisan queue:work --once --sleep=5 ``` @@ -232,7 +232,7 @@ By default `queue:work` will process jobs without ever re-booting the framework. To start a queue worker in daemon mode, simply omit the `--once` flag: -``` +```bash php artisan queue:work connection php artisan queue:work connection --sleep=3 @@ -248,7 +248,7 @@ The simplest way to deploy an application using daemon queue workers is to put t The easiest way to restart your workers is to include the following command in your deployment script: -``` +```bash php artisan queue:restart ``` @@ -278,7 +278,7 @@ Below are two examples of common system daemon managers. You can register a new service to run the queue worker by running the following command as the webhost user in your CLI terminal: -``` +```bash systemctl --user edit --force --full queue-worker.service ``` @@ -303,13 +303,13 @@ You should use your project's root folder as the `WorkingDirectory` definition. Once you save your configuration, you will need to enable it. -``` +```bash systemctl --user enable queue-worker.service ``` If you make any changes to your service's configuration, you must reload the configuration in `systemd`, which can be done by running the following: -``` +```bash systemctl --user daemon-reload ``` @@ -317,19 +317,19 @@ systemctl --user daemon-reload To start your queue worker daemon, simply run the following: -``` +```bash systemctl --user start queue-worker.service ``` And to stop the queue worker daemon: -``` +```bash systemctl --user stop queue-worker.service ``` Finally, to restart it: -``` +```bash systemctl --user restart queue-worker.service ``` @@ -339,7 +339,7 @@ systemctl --user restart queue-worker.service If you wish to check on the status of your queue worker, you can run the following: -``` +```bash systemctl --user status queue-worker.service ``` @@ -347,7 +347,7 @@ By default, `systemd` will show whether your service is active or not, and provi You can also get the full logs by querying `journalctl`: -``` +```bash journalctl --user -u queue-worker.service ``` @@ -356,7 +356,7 @@ journalctl --user -u queue-worker.service Supervisor is a process monitor for the Linux operating system, and will automatically restart your `queue:work` process if it fails. To install Supervisor on Ubuntu, you may use the following command: -``` +```bash sudo apt-get install supervisor ``` @@ -382,7 +382,7 @@ In this example, the `numprocs` directive will instruct Supervisor to run 8 `que Once the configuration file has been created, you may update the Supervisor configuration and start the processes using the following commands: -``` +```bash sudo supervisorctl reread sudo supervisorctl update @@ -399,7 +399,7 @@ Since things don't always go as planned, sometimes your queued jobs will fail. D You can specify the maximum number of times a job should be attempted using the `--tries` switch on the `queue:work` command: -``` +```bash php artisan queue:work connection-name --tries=3 ``` @@ -426,24 +426,24 @@ The original array of `data` will also be automatically passed onto the failed m To view all of your failed jobs, you may use the `queue:failed` Artisan command: -``` +```bash php artisan queue:failed ``` The `queue:failed` command will list the job ID, connection, queue, and failure time. The job ID may be used to retry the failed job. For instance, to retry a failed job that has an ID of 5, the following command should be issued: -``` +```bash php artisan queue:retry 5 ``` If you would like to delete a failed job, you may use the `queue:forget` command: -``` +```bash php artisan queue:forget 5 ``` To delete all of your failed jobs, you may use the `queue:flush` command: -``` +```bash php artisan queue:flush ``` diff --git a/services-request-input.md b/services-request-input.md index e82129f5..51ac4a24 100644 --- a/services-request-input.md +++ b/services-request-input.md @@ -13,31 +13,43 @@ You may access all user input with a few simple methods. You do not need to worr #### Retrieving an input value - $name = Input::get('name'); +```php +$name = Input::get('name'); +``` #### Retrieving a default value if the input value is absent - $name = Input::get('name', 'Sally'); +```php +$name = Input::get('name', 'Sally'); +``` #### Determining if an input value is present - if (Input::has('name')) { - // - } +```php +if (Input::has('name')) { + // +} +``` #### Getting all input for the request - $input = Input::all(); +```php +$input = Input::all(); +``` #### Getting only some of the request input - $input = Input::only('username', 'password'); +```php +$input = Input::only('username', 'password'); - $input = Input::except('credit_card'); +$input = Input::except('credit_card'); +``` When working on forms with "array" inputs, you may use dot notation to access the arrays: - $input = Input::get('products.0.name'); +```php +$input = Input::get('products.0.name'); +``` > **NOTE:** Some JavaScript libraries such as Backbone may send input to the application as JSON. You may access this data via `Input::get` like normal. @@ -50,23 +62,31 @@ By default, all cookies created by the Winter are encrypted and signed with an a #### Retrieving a cookie value - $value = Cookie::get('name'); +```php +$value = Cookie::get('name'); +``` #### Attaching a new cookie to a response - $response = Response::make('Hello World'); +```php +$response = Response::make('Hello World'); - $response->withCookie(Cookie::make('name', 'value', $minutes)); +$response->withCookie(Cookie::make('name', 'value', $minutes)); +``` #### Queueing a cookie for the next response If you would like to set a cookie before a response has been created, use the `Cookie::queue` method. The cookie will automatically be attached to the final response from your application. - Cookie::queue($name, $value, $minutes); +```php +Cookie::queue($name, $value, $minutes); +``` #### Creating a cookie that lasts forever - $cookie = Cookie::forever('name', 'value'); +```php +$cookie = Cookie::forever('name', 'value'); +``` #### Handling cookies without encryption @@ -75,28 +95,32 @@ This is useful, for example, when you want to pass data from frontend to server Add names of the cookies that should not be encrypted or decrypted to `unencryptedCookies` parameter in the `config/cookie.php` configuration file. - /* - |-------------------------------------------------------------------------- - | Cookies without encryption - |-------------------------------------------------------------------------- - | - | Winter CMS encrypts/decrypts cookies by default. You can specify cookies - | that should not be encrypted or decrypted here. This is useful, for - | example, when you want to pass data from frontend to server side backend - | via cookies, and vice versa. - | - */ - - 'unencryptedCookies' => [ - 'my_cookie', - ], +```php +/* +|-------------------------------------------------------------------------- +| Cookies without encryption +|-------------------------------------------------------------------------- +| +| Winter CMS encrypts/decrypts cookies by default. You can specify cookies +| that should not be encrypted or decrypted here. This is useful, for +| example, when you want to pass data from frontend to server side backend +| via cookies, and vice versa. +| +*/ + +'unencryptedCookies' => [ + 'my_cookie', +], +``` Alternatively for plugins, you can also add these dynamically from `Plugin.php` of your plugin. - public function boot() - { - Config::push('cookie.unencryptedCookies', "my_cookie"); - } +```php +public function boot() +{ + Config::push('cookie.unencryptedCookies', "my_cookie"); +} +``` ## Old input @@ -105,72 +129,98 @@ You may need to keep input from one request until the next request. For example, #### Flashing input to the session - Input::flash(); +```php +Input::flash(); +``` #### Flashing only some input to the session - Input::flashOnly('username', 'email'); +```php +Input::flashOnly('username', 'email'); - Input::flashExcept('password'); +Input::flashExcept('password'); +``` Since you often will want to flash input in association with a redirect to the previous page, you may easily chain input flashing onto a redirect. - return Redirect::to('form')->withInput(); +```php +return Redirect::to('form')->withInput(); - return Redirect::to('form')->withInput(Input::except('password')); +return Redirect::to('form')->withInput(Input::except('password')); +``` > **NOTE:** You may flash other data across requests using the [Session](../services/session) class. #### Retrieving old data - Input::old('username'); +```php +Input::old('username'); +``` ## Files #### Retrieving an uploaded file - $file = Input::file('photo'); +```php +$file = Input::file('photo'); +``` #### Determining if a file was uploaded - if (Input::hasFile('photo')) { - // - } +```php +if (Input::hasFile('photo')) { + // +} +``` The object returned by the `file` method is an instance of the `Symfony\Component\HttpFoundation\File\UploadedFile` class, which extends the PHP `SplFileInfo` class and provides a variety of methods for interacting with the file. #### Determining if an uploaded file is valid - if (Input::file('photo')->isValid()) { - // - } +```php +if (Input::file('photo')->isValid()) { + // +} +``` #### Moving an uploaded file - Input::file('photo')->move($destinationPath); +```php +Input::file('photo')->move($destinationPath); - Input::file('photo')->move($destinationPath, $fileName); +Input::file('photo')->move($destinationPath, $fileName); +``` #### Retrieving the path to an uploaded file - $path = Input::file('photo')->getRealPath(); +```php +$path = Input::file('photo')->getRealPath(); +``` #### Retrieving the original name of an uploaded file - $name = Input::file('photo')->getClientOriginalName(); +```php +$name = Input::file('photo')->getClientOriginalName(); +``` #### Retrieving the extension of an uploaded file - $extension = Input::file('photo')->getClientOriginalExtension(); +```php +$extension = Input::file('photo')->getClientOriginalExtension(); +``` #### Retrieving the size of an uploaded file - $size = Input::file('photo')->getSize(); +```php +$size = Input::file('photo')->getSize(); +``` #### Retrieving the MIME type of an uploaded file - $mime = Input::file('photo')->getMimeType(); +```php +$mime = Input::file('photo')->getMimeType(); +``` ## Request information @@ -179,66 +229,90 @@ The `Request` class provides many methods for examining the HTTP request for you #### Retrieving the request URI - $uri = Request::path(); +```php +$uri = Request::path(); +``` #### Retrieving the request method - $method = Request::method(); +```php +$method = Request::method(); - if (Request::isMethod('post')) { - // - } +if (Request::isMethod('post')) { + // +} +``` #### Determining if the request path matches a pattern - if (Request::is('admin/*')) { - // - } +```php +if (Request::is('admin/*')) { + // +} +``` #### Get the request URL - $url = Request::url(); +```php +$url = Request::url(); +``` #### Retrieve a request URI segment - $segment = Request::segment(1); +```php +$segment = Request::segment(1); +``` #### Retrieving a request header - $value = Request::header('Content-Type'); +```php +$value = Request::header('Content-Type'); +``` #### Retrieving values from $_SERVER - $value = Request::server('PATH_INFO'); +```php +$value = Request::server('PATH_INFO'); +``` #### Determining if the request is over HTTPS - if (Request::secure()) { - // - } +```php +if (Request::secure()) { + // +} +``` #### Determine if the request is using AJAX - if (Request::ajax()) { - // - } +```php +if (Request::ajax()) { + // +} +``` #### Determine if the request has JSON content type - if (Request::isJson()) { - // - } +```php +if (Request::isJson()) { + // +} +``` #### Determine if the request is asking for JSON - if (Request::wantsJson()) { - // - } +```php +if (Request::wantsJson()) { + // +} +``` #### Checking the requested response format The `Request::format` method will return the requested response format based on the HTTP Accept header: - if (Request::format() == 'json') { - // - } +```php +if (Request::format() == 'json') { + // +} +``` diff --git a/services-response-view.md b/services-response-view.md index c930f2db..86611117 100644 --- a/services-response-view.md +++ b/services-response-view.md @@ -23,61 +23,77 @@ A response can be returned from almost PHP method that is used by the page. This Returning a string from a CMS page, layout or component method will halt the process at this point and override the default behavior, so here it will display the "Hello World" string instead of displaying the page. - public function onStart() - { - return 'Hello World'; - } +```php +public function onStart() +{ + return 'Hello World'; +} +``` #### Returning strings from AJAX handlers Returning a string from an AJAX handler will add the string to the response collection using the default key of `result`. Requested partials will still be included in the response. - public function onDoSomething() - { - return 'Hello World'; - // ['result' => 'Hello World'] - } +```php +public function onDoSomething() +{ + return 'Hello World'; + // ['result' => 'Hello World'] +} +``` #### Returning strings from routes Returning a string from a [route definition](../services/router) will act the same as a CMS method and display the string as the response. - Route::get('/', function() { - return 'Hello World'; - }); +```php +Route::get('/', function() { + return 'Hello World'; +}); +``` #### Creating custom responses For a more robust solution, returning a `Response` object providing a variety of methods for building HTTP responses. We will explore this topic further in this article. - $contents = 'Page not found'; - $statusCode = 404; - return Response::make($contents, $statusCode); +```php +$contents = 'Page not found'; +$statusCode = 404; +return Response::make($contents, $statusCode); +``` ### Attaching headers to responses Keep in mind that most response methods are chainable, allowing for the fluent building of responses. For example, you may use the `header` method to add a series of headers to the response before sending it back to the user: - return Response::make($content) - ->header('Content-Type', $type) - ->header('X-Header-One', 'Header Value') - ->header('X-Header-Two', 'Header Value'); +```php +return Response::make($content) + ->header('Content-Type', $type) + ->header('X-Header-One', 'Header Value') + ->header('X-Header-Two', 'Header Value'); +``` A practical example of this could be returning an XML response: - return Response::make($xmlString)->header('Content-Type', 'text/xml'); +```php +return Response::make($xmlString)->header('Content-Type', 'text/xml'); +``` ### Attaching cookies to responses The `withCookie` method allows you to easily attach cookies to the response. For example, you may use the withCookie method to generate a cookie and attach it to the response instance: - return Response::make($content)->withCookie('name', 'value'); +```php +return Response::make($content)->withCookie('name', 'value'); +``` The `withCookie` method accepts additional optional arguments which allow you to further customize your cookie's properties: - ->withCookie($name, $value, $minutes, $path, $domain, $secure, $httpOnly) +```php +->withCookie($name, $value, $minutes, $path, $domain, $secure, $httpOnly) +``` ## Other response types @@ -89,30 +105,38 @@ The `Response` facade may be used to conveniently generate other types of respon If you need access to the `Response` class methods, but want to return a [view](#views) as the response content, you may use the `Response::view` method for convenience: - return Response::view('acme.blog::hello')->header('Content-Type', $type); +```php +return Response::view('acme.blog::hello')->header('Content-Type', $type); +``` ### JSON responses The `json` method will automatically set the `Content-Type` header to application/json, as well as convert the given array into JSON using the `json_encode` PHP function: - return Response::json(['name' => 'Steve', 'state' => 'CA']); +```php +return Response::json(['name' => 'Steve', 'state' => 'CA']); +``` If you would like to create a JSONP response, you may use the `json` method in addition to `setCallback`: - return Response::json(['name' => 'Steve', 'state' => 'CA']) - ->setCallback(Input::get('callback')); +```php +return Response::json(['name' => 'Steve', 'state' => 'CA']) + ->setCallback(Input::get('callback')); +``` ### File downloads The `download` method may be used to generate a response that forces the user's browser to download the file at the given path. The `download` method accepts a file name as the second argument to the method, which will determine the file name that is seen by the user downloading the file. Finally, you may pass an array of HTTP headers as the third argument to the method: - return Response::download($pathToFile); +```php +return Response::download($pathToFile); - return Response::download($pathToFile, $name, $headers); +return Response::download($pathToFile, $name, $headers); - return Response::download($pathToFile)->deleteFileAfterSend(true); +return Response::download($pathToFile)->deleteFileAfterSend(true); +``` > **NOTE:** Symfony HttpFoundation, which manages file downloads, requires the file being downloaded to have an ASCII file name. @@ -121,14 +145,18 @@ The `download` method may be used to generate a response that forces the user's Redirect responses are typically instances of the `Illuminate\Http\RedirectResponse` class, and contain the proper headers needed to redirect the user to another URL. The simplest way to generate a `RedirectResponse` instance is to use the `to` method on the `Redirect` facade. - return Redirect::to('user/login'); +```php +return Redirect::to('user/login'); +``` ### Returning a redirect with flash data Redirecting to a new URL and [flashing data to the session](../services/session) are typically done at the same time. So, for convenience, you may create a `RedirectResponse` instance and flash data to the session in a single method chain: - return Redirect::to('user/login')->with('message', 'Login Failed'); +```php +return Redirect::to('user/login')->with('message', 'Login Failed'); +``` > **NOTE:** Since the `with` method flashes data to the session, you may retrieve the data using the typical `Session::get` method. @@ -137,29 +165,37 @@ Redirecting to a new URL and [flashing data to the session](../services/session) You may wish to redirect the user to their previous location, for example, after a form submission. You can do so by using the `back` method: - return Redirect::back(); +```php +return Redirect::back(); - return Redirect::back()->withInput(); +return Redirect::back()->withInput(); +``` #### Redirecting to the current page Sometimes you want to simply refresh the current page, you can do this using the `refresh` method: - return Redirect::refresh(); +```php +return Redirect::refresh(); +``` ## Response macros If you would like to define a custom response that you can re-use in a variety of your routes and controllers, you may use the `Response::macro` method: - Response::macro('caps', function($value) { - return Response::make(strtoupper($value)); - }); +```php +Response::macro('caps', function($value) { + return Response::make(strtoupper($value)); +}); +``` The `macro` function accepts a name as its first argument, and a Closure as its second. The macro's Closure will be executed when calling the macro name on the `Response` class: - return Response::caps('foo'); +```php +return Response::caps('foo'); +``` You may define your macros 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 macro registrations. @@ -170,27 +206,33 @@ Views are a great way to store system based presentation logic, such as markup u A simple view could look something like this: - +```twig + - - -

    Hello, {{ name }}

    - - + + +

    Hello, {{ name }}

    + + +``` Views can also be parsed using PHP templating by using the `.php` extension: - +```php + - - -

    Hello,

    - - + + +

    Hello,

    + + +``` This view may be returned to the browser using the `View::make` method: - return View::make('acme.blog::greeting', ['name' => 'Charlie']); +```php +return View::make('acme.blog::greeting', ['name' => 'Charlie']); +``` The first argument is a "path hint" that contains the plugin name, separated by two colons `::`, followed by the view file name. The second argument passed to `View::make` is an array of data that should be made available to the view. @@ -198,41 +240,53 @@ The first argument is a "path hint" that contains the plugin name, separated by #### Passing data to views - // Using conventional approach - $view = View::make('acme.blog::greeting')->with('name', 'Steve'); +```php +// Using conventional approach +$view = View::make('acme.blog::greeting')->with('name', 'Steve'); - // Using magic methods - $view = View::make('acme.blog::greeting')->withName('steve'); +// Using magic methods +$view = View::make('acme.blog::greeting')->withName('steve'); +``` In the example above the variable `name` would be accessible from the view, and would contain `Steve`. As above, if you want to pass an array of data, you may do so as the second argument given to the `make` method: - $view = View::make('acme.blog::greeting', $data); +```php +$view = View::make('acme.blog::greeting', $data); +``` It is also possible to share a piece of data across all views: - View::share('name', 'Steve'); +```php +View::share('name', 'Steve'); +``` #### Passing a sub-view to a view Sometimes you may wish to pass a view into another view. For example, given a sub-view stored at `plugins/acme/blog/views/child/view.php`, we could pass it to another view like so: - $view = View::make('acme.blog::greeting')->nest('child', 'acme.blog::child.view'); +```php +$view = View::make('acme.blog::greeting')->nest('child', 'acme.blog::child.view'); - $view = View::make('acme.blog::greeting')->nest('child', 'acme.blog::child.view', $data); +$view = View::make('acme.blog::greeting')->nest('child', 'acme.blog::child.view', $data); +``` The sub-view can then be rendered from the parent view: - - -

    Hello!

    - {{ child|raw }} - - +```twig + + +

    Hello!

    + {{ child | raw }} + + +``` #### Determining if a view exists If you need to check if a view exists, use the `View::exists` method: - if (View::exists('acme.blog::mail.customer')) { - // - } +```php +if (View::exists('acme.blog::mail.customer')) { + // +} +``` diff --git a/services-router.md b/services-router.md index 2c892764..ac7779f9 100644 --- a/services-router.md +++ b/services-router.md @@ -19,41 +19,49 @@ While routing is handled automatically for the [backend controllers](../backend/ You can define these routes by creating a file named **routes.php** in a same directory as the [plugin registration file](../plugin/registration). The most basic routes simply accept a URI and a `Closure`: - Route::get('/', function () { - return 'Hello World'; - }); +```php +Route::get('/', function () { + return 'Hello World'; +}); - Route::post('foo/bar', function () { - return 'Hello World'; - }); +Route::post('foo/bar', function () { + return 'Hello World'; +}); - Route::put('foo/bar', function () { - // - }); +Route::put('foo/bar', function () { + // +}); - Route::delete('foo/bar', function () { - // - }); +Route::delete('foo/bar', function () { + // +}); +``` #### Registering a route for multiple verbs Sometimes you may need to register a route that responds to multiple HTTP verbs. You may do so using the `match` method on the `Route` facade: - Route::match(['get', 'post'], '/', function () { - return 'Hello World'; - }); +```php +Route::match(['get', 'post'], '/', function () { + return 'Hello World'; +}); +``` You may even register a route that responds to all HTTP verbs using the `any` method: - Route::any('foo', function () { - return 'Hello World'; - }); +```php +Route::any('foo', function () { + return 'Hello World'; +}); +``` #### Generating URLs to routes You may generate URLs to your routes using the `Url` facade: - $url = Url::to('foo'); +```php +$url = Url::to('foo'); +``` ## Route parameters @@ -63,15 +71,19 @@ You may generate URLs to your routes using the `Url` facade: Sometimes you will need to capture segments of the URI within your route, for example, you may need to capture a user's ID from the URL. You may do so by defining route parameters: - Route::get('user/{id}', function ($id) { - return 'User '.$id; - }); +```php +Route::get('user/{id}', function ($id) { + return 'User '.$id; +}); +``` You may define as many route parameters as required by your route: - Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { - // - }); +```php +Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { + // +}); +``` Route parameters are always encased within singular *curly brackets*. The parameters will be passed into your route's `Closure` when the route is executed. @@ -82,65 +94,77 @@ Route parameters are always encased within singular *curly brackets*. The parame Occasionally you may need to specify a route parameter, but make the presence of that route parameter optional. You may do so by placing a `?` mark after the parameter name: - Route::get('user/{name?}', function ($name = null) { - return $name; - }); +```php +Route::get('user/{name?}', function ($name = null) { + return $name; +}); - Route::get('user/{name?}', function ($name = 'John') { - return $name; - }); +Route::get('user/{name?}', function ($name = 'John') { + return $name; +}); +``` ### Regular expression constraints You may constrain the format of your route parameters using the `where` method on a route instance. The `where` method accepts the name of the parameter and a regular expression defining how the parameter should be constrained: - Route::get('user/{name}', function ($name) { - // - })->where('name', '[A-Za-z]+'); +```php +Route::get('user/{name}', function ($name) { + // +})->where('name', '[A-Za-z]+'); - Route::get('user/{id}', function ($id) { - // - })->where('id', '[0-9]+'); +Route::get('user/{id}', function ($id) { + // +})->where('id', '[0-9]+'); - Route::get('user/{id}/{name}', function ($id, $name) { - // - })->where(['id' => '[0-9]+', 'name' => '[a-z]+']); +Route::get('user/{id}/{name}', function ($id, $name) { + // +})->where(['id' => '[0-9]+', 'name' => '[a-z]+']); +``` ## Named routes Named routes allow you to conveniently generate URLs or redirects for a specific route. You may specify a name for a route using the `as` array key when defining the route: - Route::get('user/profile', ['as' => 'profile', function () { - // - }]); +```php +Route::get('user/profile', ['as' => 'profile', function () { + // +}]); +``` #### Route groups & named routes If you are using [route groups](#route-groups), you may specify an `as` keyword in the route group attribute array, allowing you to set a common route name prefix for all routes within the group: - Route::group(['as' => 'admin::'], function () { - Route::get('dashboard', ['as' => 'dashboard', function () { - // Route named "admin::dashboard" - }]); - }); +```php +Route::group(['as' => 'admin::'], function () { + Route::get('dashboard', ['as' => 'dashboard', function () { + // Route named "admin::dashboard" + }]); +}); +``` #### Generating URLs to named routes Once you have assigned a name to a given route, you may use the route's name when generating URLs or redirects via the `Url::route` method: - $url = Url::route('profile'); +```php +$url = Url::route('profile'); - $redirect = Response::redirect()->route('profile'); +$redirect = Response::redirect()->route('profile'); +``` If the route defines parameters, you may pass the parameters as the second argument to the `route` method. The given parameters will automatically be inserted into the URL: - Route::get('user/{id}/profile', ['as' => 'profile', function ($id) { - // - }]); +```php +Route::get('user/{id}/profile', ['as' => 'profile', function ($id) { + // +}]); - $url = Url::route('profile', ['id' => 1]); +$url = Url::route('profile', ['id' => 1]); +``` ## Route groups @@ -152,30 +176,36 @@ Route groups allow you to share route attributes across a large number of routes Route groups may also be used to route wildcard sub-domains. Sub-domains may be assigned route parameters just like route URIs, allowing you to capture a portion of the sub-domain for usage in your route or controller. The sub-domain may be specified using the `domain` key on the group attribute array: - Route::group(['domain' => '{account}.example.com'], function () { - Route::get('user/{id}', function ($account, $id) { - // - }); +```php +Route::group(['domain' => '{account}.example.com'], function () { + Route::get('user/{id}', function ($account, $id) { + // }); +}); +``` ### Route prefixes The `prefix` group array attribute may be used to prefix each route in the group with a given URI. For example, you may want to prefix all route URIs within the group with `admin`: - Route::group(['prefix' => 'admin'], function () { - Route::get('users', function () { - // Matches The "/admin/users" URL - }); +```php +Route::group(['prefix' => 'admin'], function () { + Route::get('users', function () { + // Matches The "/admin/users" URL }); +}); +``` You may also use the `prefix` parameter to specify common parameters for your grouped routes: - Route::group(['prefix' => 'accounts/{account_id}'], function () { - Route::get('detail', function ($account_id) { - // Matches The accounts/{account_id}/detail URL - }); +```php +Route::group(['prefix' => 'accounts/{account_id}'], function () { + Route::get('detail', function ($account_id) { + // Matches The accounts/{account_id}/detail URL }); +}); +``` ### Route Middleware @@ -183,19 +213,25 @@ You may also use the `prefix` parameter to specify common parameters for your gr Registering middleware inside your plugin's `boot()` method will register it globally for each request. If you want to register middleware to one route at a time you should do it like this: - Route::get('info', 'Acme\News@info')->middleware('Path\To\Your\Middleware'); +```php +Route::get('info', 'Acme\News@info')->middleware('Path\To\Your\Middleware'); +``` For route groups it could be done like this: - Route::group(['middleware' => 'Path\To\Your\Middleware'], function() { - Route::get('info', 'Acme\News@info'); - }); +```php +Route::group(['middleware' => 'Path\To\Your\Middleware'], function() { + Route::get('info', 'Acme\News@info'); +}); +``` And finally, if you want to assign a group of middleware to just one route you can it like this - Route::middleware(['Path\To\Your\Middleware'])->group(function() { - Route::get('info', 'Acme\News@info'); - }); +```php +Route::middleware(['Path\To\Your\Middleware'])->group(function() { + Route::get('info', 'Acme\News@info'); +}); +``` You can of course add more than one middleware in a group, just one is used in the above examples for convenience. @@ -204,7 +240,9 @@ You can of course add more than one middleware in a group, just one is used in t There are two ways to manually trigger a 404 error from a route. First, you may use the `abort` helper. The `abort` helper simply throws a `Symfony\Component\HttpFoundation\Exception\HttpException` with the specified status code: - App::abort(404); +```php +App::abort(404); +``` Secondly, you may manually throw an instance of `Symfony\Component\HttpKernel\Exception\NotFoundHttpException`. diff --git a/services-session.md b/services-session.md index 11a1cd99..50c300ab 100644 --- a/services-session.md +++ b/services-session.md @@ -7,7 +7,7 @@ ## Configuration -Since HTTP driven applications are stateless, sessions provide a way to store information about the user across requests. Winter ships with a variety of session backends available for use through a clean, unified API. Support for popular backends such as [Memcached](http://memcached.org), [Redis](http://redis.io), and databases is included out of the box. +Since HTTP driven applications are stateless, sessions provide a way to store information about the user across requests. Winter ships with a variety of session backends available for use through a clean, unified API. Support for popular backends such as [Memcached](https://memcached.org), [Redis](https://redis.io), and databases is included out of the box. The session configuration is stored in `config/session.php`. Be sure to review the well documented options available to you in this file. By default, Winter is configured to use the `file` session driver, which will work well for the majority of applications. @@ -32,67 +32,87 @@ Winter uses the `flash` session key internally, so you should not add an item to Using the `Session` facade you may call a variety of functions to interact with the underlying data. For example, the `put` method stores a new piece of data in the session: - Session::put('key', 'value'); +```php +Session::put('key', 'value'); +``` #### Pushing to array session values The `push` method may be used to push a new value onto a session value that is an array. For example, if the `user.teams` key contains an array of team names, you may push a new value onto the array like so: - Session::push('user.teams', 'developers'); +```php +Session::push('user.teams', 'developers'); +``` #### Retrieving data from the session When you retrieve a value from the session, you may also pass a default value as the second argument to the `get` method. This default value will be returned if the specified key does not exist in the session. If you pass a `Closure` as the default value to the `get` method, the `Closure` will be executed and its result returned: - $value = Session::get('key'); +```php +$value = Session::get('key'); - $value = Session::get('key', 'default'); +$value = Session::get('key', 'default'); - $value = Session::get('key', function() { return 'default'; }); +$value = Session::get('key', function() { return 'default'; }); +``` #### Retrieving all data from the session If you would like to retrieve all data from the session, you may use the `all` method: - $data = Session::all(); +```php +$data = Session::all(); +``` #### Retrieving data and forgetting it The `pull` method will retrieve and delete an item from the session: - $value = Session::pull('key', 'default'); +```php +$value = Session::pull('key', 'default'); +``` #### Determining if an item exists in the session The `has` method may be used to check if an item exists in the session. This method will return `true` if the item exists: - if (Session::has('users')) { - // - } +```php +if (Session::has('users')) { + // +} +``` #### Deleting data from the session The `forget` method will remove a piece of data from the session. If you would like to remove all data from the session, you may use the `flush` method: - Session::forget('key'); +```php +Session::forget('key'); - Session::flush(); +Session::flush(); +``` #### Regenerating the session ID If you need to regenerate the session ID, you may use the `regenerate` method: - Session::regenerate(); +```php +Session::regenerate(); +``` ## Flash data Sometimes you may wish to store items in the session only for the next request. You may do so using the `Session::flash` method. Data stored in the session using this method will only be available during the subsequent HTTP request, and then will be deleted. Flash data is primarily useful for short-lived status messages: - Session::flash('key', 'value'); +```php +Session::flash('key', 'value'); +``` If you need to keep your flash data around for even more requests, you may use the `reflash` method, which will keep all of the flash data around for an additional request. If you only need to keep specific flash data around, you may use the `keep` method: - Session::reflash(); +```php +Session::reflash(); - Session::keep(['username', 'email']); +Session::keep(['username', 'email']); +``` diff --git a/services-validation.md b/services-validation.md index d78a358d..cdf6b166 100644 --- a/services-validation.md +++ b/services-validation.md @@ -22,10 +22,12 @@ The validator class is a simple, convenient facility for validating data and ret #### Basic Validation Example - $validator = Validator::make( - ['name' => 'Joe'], - ['name' => 'required|min:5'] - ); +```php +$validator = Validator::make( + ['name' => 'Joe'], + ['name' => 'required|min:5'] +); +``` The first argument passed to the `make` method is the data under validation. The second argument is the validation rules that should be applied to the data. @@ -33,39 +35,49 @@ The first argument passed to the `make` method is the data under validation. The Multiple rules may be delimited using either a "pipe" character, or as separate elements of an array. - $validator = Validator::make( - ['name' => 'Joe'], - ['name' => ['required', 'min:5']] - ); +```php +$validator = Validator::make( + ['name' => 'Joe'], + ['name' => ['required', 'min:5']] +); +``` #### Validating multiple fields - $validator = Validator::make( - [ - 'name' => 'Joe', - 'password' => 'lamepassword', - 'email' => 'email@example.com' - ], - [ - 'name' => 'required', - 'password' => 'required|min:8', - 'email' => 'required|email|unique:users' - ] - ); +```php +$validator = Validator::make( + [ + 'name' => 'Joe', + 'password' => 'lamepassword', + 'email' => 'email@example.com' + ], + [ + 'name' => 'required', + 'password' => 'required|min:8', + 'email' => 'required|email|unique:users' + ] +); +``` Once a `Validator` instance has been created, the `fails` (or `passes`) method may be used to perform the validation. - if ($validator->fails()) { - // The given data did not pass validation - } +```php +if ($validator->fails()) { + // The given data did not pass validation +} +``` If validation has failed, you may retrieve the error messages from the validator. - $messages = $validator->messages(); +```php +$messages = $validator->messages(); +``` You may also access an array of the failed validation rules, without messages. To do so, use the `failed` method: - $failed = $validator->failed(); +```php +$failed = $validator->failed(); +``` #### Validating files @@ -78,53 +90,67 @@ After calling the `messages` method on a `Validator` instance, you will receive #### Retrieving the first error message for a field - echo $messages->first('email'); +```php +echo $messages->first('email'); +``` #### Retrieving all error messages for a field - foreach ($messages->get('email') as $message) { - // - } +```php +foreach ($messages->get('email') as $message) { + // +} +``` #### Retrieving all error messages for all fields - foreach ($messages->all() as $message) { - // - } +```php +foreach ($messages->all() as $message) { + // +} +``` #### Determining if messages exist for a field - if ($messages->has('email')) { - // - } +```php +if ($messages->has('email')) { + // +} +``` #### Retrieving an error message with a format - echo $messages->first('email', '

    :message

    '); +```php +echo $messages->first('email', '

    :message

    '); +``` > **NOTE:** By default, messages are formatted using Bootstrap compatible syntax. #### Retrieving all error messages with a format - foreach ($messages->all('
  • :message
  • ') as $message) { - // - } +```php +foreach ($messages->all('
  • :message
  • ') as $message) { + // +} +``` ## Error messages & views Once you have performed validation, you will need an easy way to get the error messages back to your views. This is conveniently handled by Winter. Consider the following routes as an example: - public function onRegister() - { - $rules = []; +```php +public function onRegister() +{ + $rules = []; - $validator = Validator::make(Input::all(), $rules); + $validator = Validator::make(Input::all(), $rules); - if ($validator->fails()) { - return Redirect::to('register')->withErrors($validator); - } + if ($validator->fails()) { + return Redirect::to('register')->withErrors($validator); } +} +``` Note that when validation fails, we pass the `Validator` instance to the Redirect using the `withErrors` method. This method will flash the error messages to the session so that they are available on the next request. @@ -132,17 +158,23 @@ Winter will always check for errors in the session data, and automatically bind So, after redirection, you may utilize the automatically bound `errors` variable in your view: - {{ errors.first('email') }} +```twig +{{ errors.first('email') }} +``` ### Named error bags If you have multiple forms on a single page, you may wish to name the `MessageBag` of errors. This will allow you to retrieve the error messages for a specific form. Simply pass a name as the second argument to `withErrors`: - return Redirect::to('register')->withErrors($validator, 'login'); +```php +return Redirect::to('register')->withErrors($validator, 'login'); +``` You may then access the named `MessageBag` instance from the `$errors` variable: - {{ errors.login.first('email') }} +```twig +{{ errors.login.first('email') }} +``` ## Available validation rules @@ -296,19 +328,27 @@ The field under validation must exist on a given database table. #### Basic usage of exists rule - 'state' => 'exists:states' +```php +'state' => 'exists:states' +``` #### Specifying a custom column name - 'state' => 'exists:states,abbreviation' +```php +'state' => 'exists:states,abbreviation' +``` You may also specify more conditions that will be added as "where" clauses to the query: - 'email' => 'exists:staff,email,account_id,1' +```php +'email' => 'exists:staff,email,account_id,1' +``` Passing `NULL` as a "where" clause value will add a check for a `NULL` database value: - 'email' => 'exists:staff,email,deleted_at,NULL' +```php +'email' => 'exists:staff,email,deleted_at,NULL' +``` #### image @@ -342,7 +382,9 @@ The file under validation must have a MIME type corresponding to one of the list #### Basic usage of MIME rule - 'photo' => 'mimes:jpeg,bmp,png' +```php +'photo' => 'mimes:jpeg,bmp,png' +``` #### min:_value_ @@ -428,21 +470,29 @@ The field under validation must be unique on a given database table. If the `col #### Basic usage of unique rule - 'email' => 'unique:users' +```php +'email' => 'unique:users' +``` #### Specifying a custom column name - 'email' => 'unique:users,email_address' +```php +'email' => 'unique:users,email_address' +``` #### Forcing a unique rule to ignore a given ID - 'email' => 'unique:users,email_address,10' +```php +'email' => 'unique:users,email_address,10' +``` #### Adding additional where clauses You may also specify more conditions that will be added as "where" clauses to the query: - 'email' => 'unique:users,email_address,NULL,id,account_id,1' +```php +'email' => 'unique:users,email_address,NULL,id,account_id,1' +``` In the rule above, only rows with an `account_id` of `1` would be included in the unique check. @@ -458,9 +508,11 @@ The field under validation must be formatted as an URL. In some situations, you may wish to run validation checks against a field **only** if that field is present in the input array. To quickly accomplish this, add the `sometimes` rule to your rule list: - $v = Validator::make($data, [ - 'email' => 'sometimes|required|email', - ]); +```php +$v = Validator::make($data, [ + 'email' => 'sometimes|required|email', +]); +``` In the example above, the `email` field will only be validated if it is present in the `$data` array. @@ -468,22 +520,28 @@ In the example above, the `email` field will only be validated if it is present Sometimes you may wish to require a given field only if another field has a greater value than 100. Or you may need two fields to have a given value only when another field is present. Adding these validation rules doesn't have to be a pain. First, create a `Validator` instance with your _static rules_ that never change: - $v = Validator::make($data, [ - 'email' => 'required|email', - 'games' => 'required|numeric', - ]); +```php +$v = Validator::make($data, [ + 'email' => 'required|email', + 'games' => 'required|numeric', +]); +``` Let's assume our web application is for game collectors. If a game collector registers with our application and they own more than 100 games, we want them to explain why they own so many games. For example, perhaps they run a game re-sell shop, or maybe they just enjoy collecting. To conditionally add this requirement, we can use the `sometimes` method on the `Validator` instance. - $v->sometimes('reason', 'required|max:500', function($input) { - return $input->games >= 100; - }); +```php +$v->sometimes('reason', 'required|max:500', function($input) { + return $input->games >= 100; +}); +``` The first argument passed to the `sometimes` method is the name of the field we are conditionally validating. The second argument is the rules we want to add. If the `Closure` passed as the third argument returns `true`, the rules will be added. This method makes it a breeze to build complex conditional validations. You may even add conditional validations for several fields at once: - $v->sometimes(['reason', 'cost'], 'required', function($input) { - return $input->games >= 100; - }); +```php +$v->sometimes(['reason', 'cost'], 'required', function($input) { + return $input->games >= 100; +}); +``` > **NOTE:** The `$input` parameter passed to your `Closure` will be an instance of `Illuminate\Support\Fluent` and may be used as an object to access your input and files. @@ -492,31 +550,39 @@ The first argument passed to the `sometimes` method is the name of the field we Validating array based form input fields doesn't have to be a pain. You may use "dot notation" to validate attributes within an array. For example, if the incoming HTTP request contains a `photos[profile]` field, you may validate it like so: - $validator = Validator::make(Input::all(), [ - 'photos.profile' => 'required|image', - ]); +```php +$validator = Validator::make(Input::all(), [ + 'photos.profile' => 'required|image', +]); +``` You may also validate each element of an array. For example, to validate that each e-mail in a given array input field is unique, you may do the following: - $validator = Validator::make(Input::all(), [ - 'person.*.email' => 'email|unique:users', - 'person.*.first_name' => 'required_with:person.*.last_name', - ]); +```php +$validator = Validator::make(Input::all(), [ + 'person.*.email' => 'email|unique:users', + 'person.*.first_name' => 'required_with:person.*.last_name', +]); +``` Likewise, you may use the `*` character when specifying your validation messages in your language files, making it a breeze to use a single validation message for array based fields: - 'custom' => [ - 'person.*.email' => [ - 'unique' => 'Each person must have a unique e-mail address', - ] - ], +```php +'custom' => [ + 'person.*.email' => [ + 'unique' => 'Each person must have a unique e-mail address', + ] +], +``` You may also use "array notation" in your validation rules if you wish. These rules will be converted to "dot notation" automatically on validation. - $validator = Validator::make(Input::all(), [ - 'photos[profile]' => 'required|image', - 'person[][email]' => 'email|unique:users', - ]); +```php +$validator = Validator::make(Input::all(), [ + 'photos[profile]' => 'required|image', + 'person[][email]' => 'email|unique:users', +]); +``` ## Custom error messages @@ -525,44 +591,54 @@ If needed, you may use custom error messages for validation instead of the defau #### Passing custom messages into validator - $messages = [ - 'required' => 'The :attribute field is required.', - ]; +```php +$messages = [ + 'required' => 'The :attribute field is required.', +]; - $validator = Validator::make($input, $rules, $messages); +$validator = Validator::make($input, $rules, $messages); +``` > *Note:* The `:attribute` place-holder will be replaced by the actual name of the field under validation. You may also utilize other place-holders in validation messages. #### Other validation placeholders - $messages = [ - 'same' => 'The :attribute and :other must match.', - 'size' => 'The :attribute must be exactly :size.', - 'between' => 'The :attribute must be between :min - :max.', - 'in' => 'The :attribute must be one of the following types: :values', - ]; +```php +$messages = [ + 'same' => 'The :attribute and :other must match.', + 'size' => 'The :attribute must be exactly :size.', + 'between' => 'The :attribute must be between :min - :max.', + 'in' => 'The :attribute must be one of the following types: :values', +]; +``` #### Specifying a custom message for a given attribute Sometimes you may wish to specify a custom error messages only for a specific field: - $messages = [ - 'email.required' => 'We need to know your e-mail address!', - ]; +```php +$messages = [ + 'email.required' => 'We need to know your e-mail address!', +]; +``` #### Specifying custom messages in language files In some cases, you may wish to specify your custom messages in a language file instead of passing them directly to the `Validator`. To do so, add your messages to an array in the `lang/xx/validation.php` language file for your plugin. - return [ - 'required' => 'We need to know your e-mail address!', - 'email.required' => 'We need to know your e-mail address!', - ]; +```php +return [ + 'required' => 'We need to know your e-mail address!', + 'email.required' => 'We need to know your e-mail address!', +]; +``` Then in your call to `Validator::make` use the `Lang:get` to use your custom files. - Validator::make($formValues, $validations, Lang::get('acme.blog::validation')); +```php +Validator::make($formValues, $validations, Lang::get('acme.blog::validation')); +``` ## Custom validation rules @@ -574,15 +650,15 @@ There are a variety of helpful validation rules; however, you may wish to specif The easiest way to register custom validation rules is by adding the `registerValidationRules() : array` method in the [`Plugin.php` registration file](../plugin/registration#registration-methods) for your plugin. This method should return an array where the key is the validator rule name and the value is either a class that extends `Winter\Storm\Validation\Rule` or a callable function. The callable function receives four arguments, the name of the `$attribute` being validated, the `$value` of the attribute and an array of `$parameters` passed to the rule, and the `$validator` instance. ```php - public function registerValidationRules() - { - return [ - 'be_like_bob' => \Winter\Tester\Rules\BeLikeBobRule::class, - 'uppercase' => function ($attribute, $value, $parameters, $validator) { - return strtoupper($value) === $value; - }, - ]; - } +public function registerValidationRules() +{ + return [ + 'be_like_bob' => \Winter\Tester\Rules\BeLikeBobRule::class, + 'uppercase' => function ($attribute, $value, $parameters, $validator) { + return strtoupper($value) === $value; + }, + ]; +} ``` Another way to register custom validation rules is by extending the Validator instance via the `extend` method. In a Winter CMS plugin, this can be added to the `boot()` callback method inside your `Plugin.php` registration file. @@ -594,14 +670,14 @@ You can extend the Validator instance with your custom validation rule as a `Clo If you only need the functionality of a custom rule specified once throughout your plugin or application, you may use a Closure to define the rule. The first parameter defines the name of your rule, and the second parameter provides your Closure. ```php - use Validator; +use Validator; - public function boot() - { - Validator::extend('foo', function($attribute, $value, $parameters) { - return $value == 'foo'; - }); - } +public function boot() +{ + Validator::extend('foo', function($attribute, $value, $parameters) { + return $value == 'foo'; + }); +} ``` The custom validator Closure receives three arguments: the name of the `$attribute` being validated, the `$value` of the attribute, and an array of `$parameters` passed to the rule. @@ -615,9 +691,9 @@ Validator::extend('foo', 'FooValidator@validate'); Once the Validator has been extended with your custom rule, you will need to add it to your rules definition. For example, you may add it to the `$rules` array of your model. ```php - public $rules = [ - 'field' => 'foo' - ]; +public $rules = [ + 'field' => 'foo' +]; ``` #### Using Rule objects @@ -625,39 +701,39 @@ Once the Validator has been extended with your custom rule, you will need to add A `Rule` object represents a single reusable validation rule for your models that implements the `Illuminate\Contracts\Validation\Rule` contract. Each rule object must provide three methods: a `passes` method which determines if a given value passes validation and a `message` method which defines the default fallback error message. `Rule` objects should extend the `Winter\Storm\Validation\Rule` abstract. ```php - ### Nginx configuration @@ -56,59 +60,61 @@ There are small changes required to configure your site in Nginx. Use the following code in **server** section. If you have installed Winter into a subdirectory, replace the first `/` in location directives with the directory Winter was installed under: - location / { - # Let Winter CMS handle everything by default. - # The path not resolved by Winter CMS router will return Winter CMS's 404 page. - # Everything that does not match with the whitelist below will fall into this. - rewrite ^/.*$ /index.php last; - } - - # Pass the PHP scripts to FastCGI server - location ~ ^/index.php { - # Write your FPM configuration here - - } - - # Whitelist - ## Let Winter handle if static file not exists - location ~ ^/favicon\.ico { try_files $uri /index.php; } - location ~ ^/sitemap\.xml { try_files $uri /index.php; } - location ~ ^/robots\.txt { try_files $uri /index.php; } - location ~ ^/humans\.txt { try_files $uri /index.php; } - - # Block access to all dot files and folders except .well-known - location ~ /\.(?!well-known).* { deny all; } - - ## Let nginx return 404 if static file not exists - location ~ ^/storage/app/uploads/public { try_files $uri 404; } - location ~ ^/storage/app/media { try_files $uri 404; } - location ~ ^/storage/app/resized { try_files $uri 404; } - location ~ ^/storage/temp/public { try_files $uri 404; } - - location ~ ^/modules/.*/assets { try_files $uri 404; } - location ~ ^/modules/.*/resources { try_files $uri 404; } - location ~ ^/modules/.*/behaviors/.*/assets { try_files $uri 404; } - location ~ ^/modules/.*/behaviors/.*/resources { try_files $uri 404; } - location ~ ^/modules/.*/widgets/.*/assets { try_files $uri 404; } - location ~ ^/modules/.*/widgets/.*/resources { try_files $uri 404; } - location ~ ^/modules/.*/formwidgets/.*/assets { try_files $uri 404; } - location ~ ^/modules/.*/formwidgets/.*/resources { try_files $uri 404; } - location ~ ^/modules/.*/reportwidgets/.*/assets { try_files $uri 404; } - location ~ ^/modules/.*/reportwidgets/.*/resources { try_files $uri 404; } - - location ~ ^/plugins/.*/.*/assets { try_files $uri 404; } - location ~ ^/plugins/.*/.*/resources { try_files $uri 404; } - location ~ ^/plugins/.*/.*/behaviors/.*/assets { try_files $uri 404; } - location ~ ^/plugins/.*/.*/behaviors/.*/resources { try_files $uri 404; } - location ~ ^/plugins/.*/.*/reportwidgets/.*/assets { try_files $uri 404; } - location ~ ^/plugins/.*/.*/reportwidgets/.*/resources { try_files $uri 404; } - location ~ ^/plugins/.*/.*/formwidgets/.*/assets { try_files $uri 404; } - location ~ ^/plugins/.*/.*/formwidgets/.*/resources { try_files $uri 404; } - location ~ ^/plugins/.*/.*/widgets/.*/assets { try_files $uri 404; } - location ~ ^/plugins/.*/.*/widgets/.*/resources { try_files $uri 404; } - - location ~ ^/themes/.*/assets { try_files $uri 404; } - location ~ ^/themes/.*/resources { try_files $uri 404; } +```nginx +location / { + # Let Winter CMS handle everything by default. + # The path not resolved by Winter CMS router will return Winter CMS's 404 page. + # Everything that does not match with the whitelist below will fall into this. + rewrite ^/.*$ /index.php last; +} + +# Pass the PHP scripts to FastCGI server +location ~ ^/index.php { + # Write your FPM configuration here + +} + +# Whitelist +## Let Winter handle if static file not exists +location ~ ^/favicon\.ico { try_files $uri /index.php; } +location ~ ^/sitemap\.xml { try_files $uri /index.php; } +location ~ ^/robots\.txt { try_files $uri /index.php; } +location ~ ^/humans\.txt { try_files $uri /index.php; } + +# Block access to all dot files and folders except .well-known +location ~ /\.(?!well-known).* { deny all; } + +## Let nginx return 404 if static file not exists +location ~ ^/storage/app/uploads/public { try_files $uri 404; } +location ~ ^/storage/app/media { try_files $uri 404; } +location ~ ^/storage/app/resized { try_files $uri 404; } +location ~ ^/storage/temp/public { try_files $uri 404; } + +location ~ ^/modules/.*/assets { try_files $uri 404; } +location ~ ^/modules/.*/resources { try_files $uri 404; } +location ~ ^/modules/.*/behaviors/.*/assets { try_files $uri 404; } +location ~ ^/modules/.*/behaviors/.*/resources { try_files $uri 404; } +location ~ ^/modules/.*/widgets/.*/assets { try_files $uri 404; } +location ~ ^/modules/.*/widgets/.*/resources { try_files $uri 404; } +location ~ ^/modules/.*/formwidgets/.*/assets { try_files $uri 404; } +location ~ ^/modules/.*/formwidgets/.*/resources { try_files $uri 404; } +location ~ ^/modules/.*/reportwidgets/.*/assets { try_files $uri 404; } +location ~ ^/modules/.*/reportwidgets/.*/resources { try_files $uri 404; } + +location ~ ^/plugins/.*/.*/assets { try_files $uri 404; } +location ~ ^/plugins/.*/.*/resources { try_files $uri 404; } +location ~ ^/plugins/.*/.*/behaviors/.*/assets { try_files $uri 404; } +location ~ ^/plugins/.*/.*/behaviors/.*/resources { try_files $uri 404; } +location ~ ^/plugins/.*/.*/reportwidgets/.*/assets { try_files $uri 404; } +location ~ ^/plugins/.*/.*/reportwidgets/.*/resources { try_files $uri 404; } +location ~ ^/plugins/.*/.*/formwidgets/.*/assets { try_files $uri 404; } +location ~ ^/plugins/.*/.*/formwidgets/.*/resources { try_files $uri 404; } +location ~ ^/plugins/.*/.*/widgets/.*/assets { try_files $uri 404; } +location ~ ^/plugins/.*/.*/widgets/.*/resources { try_files $uri 404; } + +location ~ ^/themes/.*/assets { try_files $uri 404; } +location ~ ^/themes/.*/resources { try_files $uri 404; } +``` ### Lighttpd configuration @@ -119,20 +125,22 @@ If your webserver is running Lighttpd you can use the following configuration to Paste the following code in the editor and change the **host address** and **server.document-root** to match your project. - $HTTP["host"] =~ "domain.example.com" { - server.document-root = "/var/www/example/" - - url.rewrite-once = ( - "^/(plugins|modules/(system|backend|cms))/(([\w-]+/)+|/|)assets/([\w-]+/)+[-\w^&'@{}[\],$=!#().%+~/ ]+\.(jpg|jpeg|gif|png|svg|swf|avi|mpg|mpeg|mp3|flv|ico|css|js|woff|ttf)(\?.*|)$" => "$0", - "^/(system|themes/[\w-]+)/assets/([\w-]+/)+[-\w^&'@{}[\],$=!#().%+~/ ]+\.(jpg|jpeg|gif|png|svg|swf|avi|mpg|mpeg|mp3|flv|ico|css|js|woff|ttf)(\?.*|)$" => "$0", - "^/storage/app/uploads/public/[\w-]+/.*$" => "$0", - "^/storage/app/media/.*$" => "$0", - "^/storage/app/resized/.*$" => "$0", - "^/storage/temp/public/[\w-]+/.*$" => "$0", - "^/(favicon\.ico)$" => "$0", - "(.*)" => "/index.php$1" - ) - } +``` +$HTTP["host"] =~ "domain.example.com" { + server.document-root = "/var/www/example/" + + url.rewrite-once = ( + "^/(plugins|modules/(system|backend|cms))/(([\w-]+/)+|/|)assets/([\w-]+/)+[-\w^&'@{}[\],$=!#().%+~/ ]+\.(jpg|jpeg|gif|png|svg|swf|avi|mpg|mpeg|mp3|flv|ico|css|js|woff|ttf)(\?.*|)$" => "$0", + "^/(system|themes/[\w-]+)/assets/([\w-]+/)+[-\w^&'@{}[\],$=!#().%+~/ ]+\.(jpg|jpeg|gif|png|svg|swf|avi|mpg|mpeg|mp3|flv|ico|css|js|woff|ttf)(\?.*|)$" => "$0", + "^/storage/app/uploads/public/[\w-]+/.*$" => "$0", + "^/storage/app/media/.*$" => "$0", + "^/storage/app/resized/.*$" => "$0", + "^/storage/temp/public/[\w-]+/.*$" => "$0", + "^/(favicon\.ico)$" => "$0", + "(.*)" => "/index.php$1" + ) +} +``` ### IIS configuration @@ -206,35 +214,41 @@ The Winter platform and some marketplace plugins will implement changes in two s You can instruct the platform to prefer test builds from the marketplace by changing the `edgeUpdates` parameter in the `config/cms.php` configuration file. - /* - |-------------------------------------------------------------------------- - | Bleeding edge updates - |-------------------------------------------------------------------------- - | - | If you are developing with Winter, it is important to have the latest - | code base, set this value to 'true' to tell the platform to download - | and use the development copies of core files and plugins. - | - */ - - 'edgeUpdates' => false, +```php +/* +|-------------------------------------------------------------------------- +| Bleeding edge updates +|-------------------------------------------------------------------------- +| +| If you are developing with Winter, it is important to have the latest +| code base, set this value to 'true' to tell the platform to download +| and use the development copies of core files and plugins. +| +*/ + +'edgeUpdates' => false, +``` > **NOTE:** For plugin developers we recommend enabling **Test updates** for your plugins listed on the marketplace, via the Plugin Settings page. -> **NOTE:** If using [Composer](../console/commands#console-install-composer) to manage updates, then replace the default Winter CMS requirements in your `composer.json` file with the following in order to download updates directly from the develop branch. +> **NOTE:** If using [Composer](../help/using-composer) to manage updates, then replace the default Winter CMS requirements in your `composer.json` file with the following in order to download updates directly from the develop branch. - "winter/storm": "dev-develop as 1.0", - "winter/wn-system-module": "dev-develop", - "winter/wn-backend-module": "dev-develop", - "winter/wn-cms-module": "dev-develop", - "laravel/framework": "~6.0", +```json +"winter/storm": "dev-develop as 1.0", +"winter/wn-system-module": "dev-develop", +"winter/wn-backend-module": "dev-develop", +"winter/wn-cms-module": "dev-develop", +"laravel/framework": "~6.0", +``` ### Using a public folder For ultimate security in production environments you may configure your web server to use a **public/** folder to ensure only public files can be accessed. First you will need to spawn a public folder using the `winter:mirror` command. - php artisan winter:mirror public/ +```bash +php artisan winter:mirror public/ +``` This will create a new directory called **public/** in the project's base directory, from here you should modify the webserver configuration to use this new path as the home directory, also known as *wwwroot*. @@ -247,16 +261,18 @@ If you share a server with other users, you should act as if your neighbor's sit You can setup this protection in the file location `config/cms.php` in the section titled **Default permission mask**. - /* - |-------------------------------------------------------------------------- - | Default permission mask - |-------------------------------------------------------------------------- - | - | Specifies a default file and folder permission for newly created objects. - | - */ - - 'defaultMask' => ['file' => '644', 'folder' => '755'], +```php +/* +|-------------------------------------------------------------------------- +| Default permission mask +|-------------------------------------------------------------------------- +| +| Specifies a default file and folder permission for newly created objects. +| +*/ + +'defaultMask' => ['file' => '644', 'folder' => '755'], +``` > **NOTE**: Don't forget to manually check to see if the files are already set to 644, as you may need to go into your cPanel and set them. @@ -335,6 +351,7 @@ The `trustedProxyHeaders` value specifies which headers will be allowed to defin ``` > **NOTE:** Amazon Elastic Load Balancing users must use the `HEADER_X_FORWARDED_AWS_ELB` option to accept the correct headers. +> > ```php > 'trustedProxyHeaders' => Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB > ``` @@ -361,19 +378,21 @@ In both of the above examples, the environment is set to the new value `dev`. Co For example, to use a different MySQL database for the `dev` environment only, create a file called **config/dev/database.php** using this content: - [ - 'mysql' => [ - 'host' => 'localhost', - 'port' => '', - 'database' => 'database', - 'username' => 'root', - 'password' => '' - ] +```php + [ + 'mysql' => [ + 'host' => 'localhost', + 'port' => '', + 'database' => 'database', + 'username' => 'root', + 'password' => '' ] - ]; + ] +]; +``` ### Domain driven environment @@ -382,25 +401,31 @@ Winter supports using an environment detected by a specific hostname. You may pl Using this file contents below, when the application is accessed via **global.website.tld** the environment will be set to `global` and likewise for the others. - [ - 'global.website.tld' => 'global', - 'local.website.tld' => 'local', - ] - ]; +```php + [ + 'global.website.tld' => 'global', + 'local.website.tld' => 'local', + ] +]; +``` ### Converting to DotEnv configuration As an alternative to the [base environment configuration](#base-environment) you may place common values in the environment instead of using configuration files. The config is then accessed using [DotEnv](https://github.com/vlucas/phpdotenv) syntax. Run the `winter:env` command to move common config values to the environment: - php artisan winter:env +```bash +php artisan winter:env +``` This will create an **.env** file in project root directory and modify configuration files to use `env` helper function. The first argument contains the key name found in the environment, the second argument contains an optional default value. - 'debug' => env('APP_DEBUG', true), +```php +'debug' => env('APP_DEBUG', true), +``` Your `.env` file should not be committed to your application's source control, since each developer or server using your application could require a different environment configuration. diff --git a/setup-installation.md b/setup-installation.md index 7fda2b69..34b6c1cd 100644 --- a/setup-installation.md +++ b/setup-installation.md @@ -14,21 +14,21 @@ Documentation on the different ways to install Winter CMS for your next project.
    -There are two ways you can install Winter, either using the [Web-based installer](#web-based-installation) or [Command-line installation](../console/commands#console-install) instructions. Before you proceed, you should check that your server meets the minimum system requirements. +There are two ways you can install Winter, either using the [Web-based installer](#web-based-installation) or [Composer installation](../help/using-composer) instructions. Before you proceed, you should check that your server meets the minimum system requirements. ## Minimum system requirements Winter CMS has some server requirements for web hosting: -1. PHP version 7.2 or higher -1. PDO PHP Extension (and relevant driver for the database you want to connect to) -1. cURL PHP Extension -1. OpenSSL PHP Extension -1. Mbstring PHP Extension -1. ZipArchive PHP Extension -1. GD PHP Extension -1. SimpleXML PHP Extension +- PHP version 7.2 or higher +- PDO PHP Extension (and relevant driver for the database you want to connect to) +- cURL PHP Extension +- OpenSSL PHP Extension +- Mbstring PHP Extension +- ZipArchive PHP Extension +- GD PHP Extension +- SimpleXML PHP Extension Some OS distributions may require you to manually install some of the required PHP extensions. @@ -46,7 +46,7 @@ When using the SQL Server database engine, you will need to install the [group c The [Web Installer](https://github.com/wintercms/web-installer) is the recommended way to install Winter for **non-technical users**. It is simpler than the command-line installation and doesn't require any special skills. -> **NOTE:** If you are a developer, we recommend that you [install via Composer instead](../console/commands#console-install-composer) +> **NOTE:** If you are a developer, we recommend that you [install via Composer instead](../help/using-composer) 1. Prepare an empty directory on the web server that will host your Winter CMS installation. It can be a main domain, sub-domain or subfolder. 2. [Download the "install.zip" file](https://github.com/wintercms/web-installer/releases/latest) from the latest release of the Winter CMS Web Installer into this folder. @@ -55,10 +55,10 @@ The [Web Installer](https://github.com/wintercms/web-installer) is the recommend 5. In your web browser, navigate to the URL pointing to that folder, and include `/install.html` at the end of the URL. 6. Follow the instructions given in the installer. -![image](https://github.com/wintercms/docs/blob/main/images/web-installer.jpg?raw=true) {.img-responsive .frame} +![Winter CMS Installer](https://github.com/wintercms/docs/blob/main/images/web-installer.jpg?raw=true) {.img-responsive .frame} -### Troubleshooting installation +### Troubleshooting a web-based installation 1. **Unable to connect to the Winter Marketplace API**: If your server has a firewall blocking requests to port 443, you will need to allow requests and responses for this port. Contact your system administrator to allow access to this port. @@ -73,7 +73,7 @@ The [Web Installer](https://github.com/wintercms/web-installer) is the recommend ## Command-line installation -If you feel more comfortable with a command-line or want to use composer, there is a CLI install process on the [Console interface page](../console/commands#console-install). +If you feel more comfortable with a command-line or want to use Composer, there is a CLI install process on the [Using Composer page](../help/using-composer) ## Post-installation steps @@ -85,13 +85,15 @@ There are some things you may need to set up after the installation is complete. If you have used the [Wizard installer](#wizard-installation), for security reasons you should verify the installation files have been deleted. The Winter installer attempts to cleanup after itself, but you should always verify that they have been successfullly removed: - install/ <== Installation directory - install.html <== Installation script +```css + ┣ πŸ“‚ install <== Installation directory + ┣ πŸ“œ install.html <== Installation script +``` ### Review configuration -Configuration files are stored in the **config** directory of the application. While each file contains descriptions for each setting, it is important to review the [common configuration options](../setup/configuration) available for your circumstances. +Configuration files are stored in the `config` directory of the application. While each file contains descriptions for each setting, it is important to review the [common configuration options](../setup/configuration) available for your circumstances. For example, in production environments you may want to enable [CSRF protection](../setup/configuration#csrf-protection). While in development environments, you may want to enable [bleeding edge updates](../setup/configuration#edge-updates). @@ -100,17 +102,17 @@ While most configuration is optional, we strongly recommend disabling [debug mod ### Setting up the scheduler -For *scheduled tasks* to operate correctly, you should add the following Cron entry to your server. Editing the crontab is commonly performed with the command `crontab -e`. +For scheduled tasks to operate correctly, you should add the following Cron entry to your server. Editing the crontab is commonly performed with the command `crontab -e`. * * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1 -Be sure to replace **/path/to/artisan** with the absolute path to the *artisan* file in the root directory of Winter. This Cron will call the command scheduler every minute. Then Winter evaluates any scheduled tasks and runs the tasks that are due. +Be sure to replace `/path/to/artisan` with the absolute path to the `artisan` file in the root directory of Winter. This cron will call the command scheduler every minute, in which Winter will evaluate any scheduled tasks and run the tasks that are due. -> **NOTE**: If you are adding this to `/etc/cron.d` you'll need to specify a user immediately after `* * * * *`. +> **NOTE**: If you are adding this to `/etc/cron.d`, you'll need to specify a user immediately after `* * * * *`. ### Setting up queue workers -You may optionally set up an external queue for processing *queued jobs*, by default these will be handled asynchronously by the platform. This behavior can be changed by setting the `default` parameter in the `config/queue.php`. +You may optionally set up an external queue for processing queued jobs. By default, these will be handled asynchronously by the platform. This behavior can be changed by setting the `default` parameter in the `config/queue.php`. If you decide to use the `database` queue driver, it is a good idea to add a Crontab entry for the command `php artisan queue:work --once` to process the first available job in the queue. diff --git a/snowboard-data-attributes.md b/snowboard-data-attributes.md new file mode 100644 index 00000000..af623de3 --- /dev/null +++ b/snowboard-data-attributes.md @@ -0,0 +1,100 @@ +# AJAX Requests (Data Attributes API) + +- [Introduction](#introduction) +- [Available Data Attributes](#available-attributes) +- [Usage Examples](#usage-examples) + + +## Introduction + +The Data Attributes API is the simpler way of embedding AJAX functionality in your themes and plugins, and removes the need to be experienced with JavaScript. While the [JavaScript API](../snowboard/request) has had numerous changes from the original [AJAX framework](../ajax/introduction), the Data Attributes API has remain largely unchanged, despite being powered by the new Snowboard framework under the hood. + +It can be loaded by adding the following tag into your CMS Theme's page or layout: + +```twig +{% snowboard request attr %} +``` + +> **NOTE:** As per the [Migration Guide](../snowboard/migration-guide), arbitrary JavaScript is no longer allowed through the Data Attributes API. Thus, the `data-request-before-update`, `data-request-success`, `data-request-error` and `data-request-complete` attributes are no longer supported. Please use the [JavaScript API](../snowboard/request) if you require this functionality. + + +## Available Data Attributes + +Triggering an AJAX request from a valid element is as simple as adding the `data-request` attribute to that element. This generally should be done on a button, link, or form. You can also customize the AJAX request using the following attributes: + + +
    + +Attribute | Description +--------- | ----------- +`data-request` | Specifies the AJAX handler name to target for the request. +`data-request-confirm` | Specifies the confirmation message to present to the user before proceeding with the request. If the user cancels, the request is not sent. +`data-request-redirect` | Specifies a URL to redirect the browser to, if a successful AJAX response is received. +`data-request-url` | Specifies the URL to send the AJAX request to. By default, this will be the current URL. +`data-request-update` | Specifies a list of partials and page elements (CSS selectors) to update on a successful AJAX response. The format is as follows: `partial: selector, partial: selector`. Usage of quotes is required in most cases: `'partial': 'selector'`. If the selector is prepended with an `@` symbol, the content received from the server will be appended to the element. If the selector is prepended with a `^` symbol, the content will be prepended. Otherwise, received content will replace the original content in the element. +`data-request-data` | Specifies additional data to send with the request to the server. The format is as follows: `'var': 'value', 'var2': 'new value'`. You may also specify this same attribute on any parent elements of the triggering element, and this data will be merged with the parent data (with the triggering data taking preference). It will also be merged with any form data, if this request triggers within a form. +`data-request-form` | Specifies the form that the AJAX request will include its data from. If this is unspecified, the closest form will be used, or if the element itself is a form, then this will be used. +`data-request-flash` | Specifies if flash messages will be accepted from the response. +`data-request-files` | Specifies if file data will be included in the request. This will allow any file inputs in the form to work. +`data-browser-validate` | Specifies if the in-built browser validation will be triggered. If present, the request will be cancelled if the browser validation fails. +`data-track-input` | Specifies if an input will trigger an AJAX request anytime the input changes. An optional number can be specified in this attribute, which represents the amount of milliseconds between any change and the AJAX request triggering. + + +When the `data-request` attribute is specified for an element, the element triggers an AJAX request when a user interacts with it. Depending on the type of element, the request is triggered on the following events: + +Element | Event +------------- | ------------- +**Forms** | when the form is submitted. +**Links, buttons** | when the element is clicked. +**Text, number, and password fields** | when the text is changed and only if the `data-track-input` attribute is presented. +**Dropdowns, checkboxes, radios** | when the element is selected. + + +## Usage examples + +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 + +``` + +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/snowboard-extras.md b/snowboard-extras.md new file mode 100644 index 00000000..e120bf21 --- /dev/null +++ b/snowboard-extras.md @@ -0,0 +1,203 @@ +# Extra UI Features + +- [Introduction](#introduction) +- [Loading indicator](#loader-stripe) +- [Loading button](#loader-button) +- [Flash messages](#ajax-flash) +- [Form validation](#ajax-validation) + - [Throwing a validation error](#throw-validation-exception) + - [Displaying error messages](#error-messages) + - [Displaying errors with fields](#field-errors) + - [Usage examples](#usage-examples) + + +## Introduction + +When using the Snowboard framework, you have the option to specify the `extras` flag which includes additional UI features. These features are often useful when working with AJAX requests in frontend CMS pages. + +```twig +{% snowboard extras %} +``` + + +## Loading indicator + +The loading indicator is a loading bar that is displayed on the top of the page when an AJAX request runs. The indicator hooks in to [global events](../snowboard/request#global-events) used by the Snowboard framework. + +When an AJAX request starts, the `ajaxPromise` event is fired. This displays the loading indicator at the top of the page. When this promise is resolved, the loading bar is removed. + + +## 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. + +```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. + +```php +function onSuccess() +{ + Flash::success('You did it!'); +} +``` + +When using AJAX Flash messages you should also ensure that your theme supports [standard flash messages](../markup/tag-flash) by placing the following code in your page or layout in order to render Flash messages that haven't been displayed yet when the page loads. + +```twig +{% flash %} +

    + {{ message }} +

    +{% endflash %} +``` + + +## Form validation + +You may specify the `data-request-validate` attribute on a form to enable server-side validation features with fields and forms. + +```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. The exception should be provided an array, which states the field names for the keys, and the error messages for the values. + +```php +function onSubmit() +{ + throw new ValidationException(['name' => 'You must give a name!']); +} +``` + +> **NOTE**: You can also pass a [Validator](../services/validation) instance as the first argument of the exception instead, to use the in-built validation service. + + +### Displaying error messages + +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 +
    +

    +
    +``` + +The `handleValidationErrors` callback, and the `ajaxValidationErrors` global event, that are available with the [Request API](../snowboard/request#global-events) allow you to fully customise the client-side validation handling. The `handleValidationErrors` callback can be used to control validation per request, while the `ajaxValidationErrors` global event can be used by [Snowboard plugins](../snowboard/plugin-development) to augment the client-side validation in a global fashion. + + +### 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. + +```html +
    + Oops.. phone number is invalid! +
    +``` + + +### Usage examples + +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. + +```php +function onDoSomething() +{ + $data = post(); + + $rules = [ + 'name' => 'required', + 'email' => 'required|email', + ]; + + $validation = Validator::make($data, $rules); + + if ($validation->fails()) { + throw new ValidationException($validation); + } + + Flash::success('Jobs done!'); +} +``` diff --git a/snowboard-handlers.md b/snowboard-handlers.md new file mode 100644 index 00000000..3c67f8ad --- /dev/null +++ b/snowboard-handlers.md @@ -0,0 +1,156 @@ +# Server-side Event Handlers + +- [Introduction](#introduction) + - [Calling a handler](#calling-handlers) + - [Generic handler](#generic-handler) +- [Redirects in AJAX handlers](#redirects-in-handlers) +- [Returning data from AJAX handlers](#returning-data-from-handlers) +- [Throwing an AJAX exception](#throw-ajax-exception) +- [Running code before handlers](#before-handler) + + +## Introduction + +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) and are used to execute the server-side functionality of an AJAX request made by the [Request API](../snowboard/request) or [Data Attributes API](../snowboard/data-attributes). + +Handler method names should be specified with the `on` prefix, followed by the event name in PascalCase - for example, `onMyHandler` or `onCreatePost`. + +All handlers support the use of [updating partials](#updating-partials) as part of the AJAX response. This behavior can also be controlled via the `update` option in the [Request API](../snowboard/request) or the `data-request-update` attribute in the [Data Attributes API](../snowboard/data-attributes). + +```php +function onSubmitContactForm() +{ + // AJAX handler functionality goes here +} +``` + +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. + + +### Calling a handler + +Every AJAX request should specify a handler name. When the request is made, the server will search all the registered handlers and run the handler with the highest priority. + +```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 should use the [`__SELF__`](../plugin/components#referencing-self) variable instead of the hard coded alias in order to support multiple instances of your component existing on the same page. + +```twig +
    +``` + +### Generic handler + +Sometimes you may need to make an AJAX request for the sole purpose of updating page contents by pulling partial updates without executing any code. You may use the `onAjax` handler for this purpose. This handler is available everywhere the AJAX framework can respond. + +#### `clock.htm` Partial +```twig +The time is {{ 'now' | date('H:i:s') }} +``` + +#### `index.htm` Page +```twig + + {% partial 'clock' %} + + +``` + + +## 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. + +```php +function onRedirectMe() +{ + return Redirect::to('http://google.com'); +} +``` + +You may also specify a `redirect` in the Request API options, or through the `data-request-redirect` data attribute. This setting will take precedence over any redirect returned in the AJAX response. + + +## Returning data from AJAX handlers + +You may want to return structured, arbitrary data from your AJAX handlers. If an AJAX handler returns an array, you can access its elements in the `success` callback handler. + +```php +function onFetchDataFromServer() +{ + /* Some server-side code */ + + return [ + 'totalUsers' => 1000, + 'totalProjects' => 937 + ]; +} +``` + +Then, in JavaScript: + +```js +Snowboard.request(this, 'onHandleForm', { + success: function(data) { + console.log(data); + } +}); +``` + +Data returned in this fashion **cannot** be accessed through the [Data Attributes API](../snowboard/data-attributes). + +You may also retrieve the data in [several events](../snowboard/request#global-events) that fire as part of the Request lifecycle. + + +## 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. + +```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. + + +## Running code before handlers + +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. + +```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). + +```php +function init() +{ + // From a component or widget class +} +``` diff --git a/snowboard-introduction.md b/snowboard-introduction.md new file mode 100644 index 00000000..6c5c6fe9 --- /dev/null +++ b/snowboard-introduction.md @@ -0,0 +1,59 @@ +# Snowboard.js - Winter JavaScript Framework + +- [Introduction](#introduction) +- [Features](#features) +- [Including the framework](#framework-script) +- [Concepts](#concepts) + +![image](https://github.com/wintercms/docs/blob/main/images/header-snowboard.png?raw=true) {.img-responsive .frame} + + +## Introduction + +Winter includes an optional JavaScript framework called **Snowboard**, which acts as an upgrade to the previous [AJAX Framework](../ajax/introduction) and provides many new useful features in an extensible fashion, whilst dropping previous hard dependencies to supercharge your projects even further. + +The framework takes advantage of the incredible enhancements made to the JavaScript ecosystem in recent years to provide a unique experience, available only on Winter. + + +## Features + +- Rewritten AJAX and JavaScript framework, built from the ground-up using the latest JavaScript syntax (ES2015+) and functionality. +- No dependency on jQuery, allowing the framework to be used across a wide variety of JavaScript projects. +- Easy, comprehensive extensibility and event handling. +- Small footprint and full control over which core functionalities to include ensures your website loads quick. + + +## Including the framework + +> Before proceeding, please read the [Migration Guide](../snowboard/migration-guide), especially if you intend to use this framework on an existing project. + +Snowboard can be included in your [CMS theme](../cms/themes) by placing the `{% snowboard %}` tag anywhere inside your [page](../cms/pages) or [layout](../cms/layouts) where you would like the JavaScript assets to be loaded - generally, this should be at the bottom of the page before the closing `` tag. You must use this tag *before* you load any assets that rely on the framework, such as plugins or event listeners, and it should also be located before the `{% scripts %}` tag to allow third party code (i.e. [Winter Plugins](../plugin/registration#Introduction)) to provide [Snowboard plugins](plugin-development) if they wish. + +By default, only the base Snowboard framework and [necessary utilties](../snowboard/utilities) are included by the `{% snowboard %}` token in order to allow for complete control over which additional features (such as the AJAX framework) are desired to be included in your themes. + +You can specify further attributes to the tag to include optional additional functionality for the framework: + +Attribute | Includes +--------- | -------- +`all` | Includes all available plugins +`request` | The base [JavaScript AJAX](../snowboard/request) request functionality +`attr` | The [HTML data attribute](../snowboard/data-attributes) request functionality +`extras` | [Several useful UI enhancements](../snowboard/extras), including flash messages, loading states and transitions. + +To add Snowboard to your theme with all of its features enabled, you would use the following: + +```twig +{% snowboard all %} +``` + +To include the framework with just the JavaScript AJAX request functionality: + +```twig +{% snowboard request %} +``` + +Or to include both the JavaScript AJAX and HTML data attribute request functionality: + +```twig +{% snowboard request attr %} +``` diff --git a/snowboard-migration-guide.md b/snowboard-migration-guide.md new file mode 100644 index 00000000..93bc161d --- /dev/null +++ b/snowboard-migration-guide.md @@ -0,0 +1,93 @@ +# Migration Guide + +- [Introduction](#introduction) +- [Breaking changes](#breaking-changes) + - [Browser support is more strict](#browser-support) + - [jQuery is no longer required](#no-jquery) + - [JavaScript in the HTML data attribute framework is deprecated](#html-callbacks) + - [AJAX events are triggered as DOM events](#ajax-dom-events) +- [Other changes](#other-changes) + - [JavaScript AJAX Requests](#js-requests) + + +## Introduction + +While care has been given to ensure that the Snowboard framework covers the entire scope of functionality that the original [AJAX framework](../ajax/introduction) provided, there are subtle differences between the two frameworks. Please take the time to read through this document to ensure that you are across the changes, especially if you intend to upgrade an existing project to use this new framework. + + +## Breaking changes + + +### Browser support is more strict + +Snowboard drops support for Internet Explorer, as well as some less-used, or discontinued, browsers such as the Samsung Internet Browser and Opera Mini. The framework targets, at the very least, support for the ECMAScript 2015 (ES2015) JavaScript language. + +Our build script is set up to consider the following browsers as compatible with the framework: + +- The browser must have at least a 0.5% market share. +- The browser must be within the last 4 released versions of that browser. +- The browser must NOT be Internet Explorer. +- The browser must NOT be discontinued by the developer. + +For people who wish to support older browsers such as Internet Explorer, you may continue to use the original [AJAX framework](../ajax/introduction), which is still supported by the Winter maintainer team, but will likely not be receiving any new features going forward. + + +### jQuery is no longer required + +We have removed the hard dependency with jQuery, which also means that no jQuery functionality exists in this new framework. If you relied on jQuery being available for your own JavaScript functionality, you must include jQuery yourself in your theme. + + +### JavaScript in the HTML data attribute framework is deprecated + +The original [AJAX framework](../ajax/attributes-api#data-attributes) allowed for arbitrary JavaScript code to be specified within the callback data attributes, for example, `data-request-success`, `data-request-error` and `data-request-complete`, as a way of allowing JavaScript to run additional tasks depending on the success or failure of an AJAX request made through the HTML data attributes. + +We have dropped support of this feature due to its use of the `eval()` method in JavaScript to execute this JavaScript, which has security implications (especially on front-facing code) and prevents people from using content security policies on their sites without the use of the `unsafe-eval` [CSP rule](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src). + +If you wish to use JavaScript with the AJAX functionality, you must either use the [JavaScript Request functionality](../snowboard/request), or use the original [AJAX framework](../ajax/introduction) which retains this feature. + + +### AJAX events are triggered as DOM events + +Previously, the original AJAX framework used jQuery's Event system to trigger events on elements that are affected by an AJAX request. As jQuery is no longer used, we now use DOM events in their place. + +This change requires us to provide event data as properties of the DOM event, not as handler parameters. + +For example, the `ajaxAlways` event which is triggered on an element when an AJAX request is triggered on an element could have a listener set up through jQuery as follows: + +```js +$('#element').on('ajaxAlways', function (event, context, data, status, xhr) { + console.log(context); // The Request's context + console.log(data); // Data returned from the AJAX response +}); +``` + +Now, you must look at the Event object properties for this information: + +```js +$('#element').on('ajaxAlways', function (event) { + console.log(event.request); // The Request object + console.log(event.responseData); // Data returned from the AJAX response +}); +``` + +Please review the [JavaScript Request](../snowboard/request) documentation for information on what properties are available for DOM events. + + +## Other changes + + +### JavaScript AJAX Requests + +#### Making a request + +The original framework used a jQuery extension to call AJAX requests via JavaScript: + +```js +$('#element').request('onAjaxHandler', { /* ... options .. */ }) +``` + +This is now changed to use the base Winter class to call the Request plugin: + +```js +Snowboard.request('#element', 'onAjaxHandler', { /* ... options .. */ }); +``` \ No newline at end of file diff --git a/snowboard-plugin-development.md b/snowboard-plugin-development.md new file mode 100644 index 00000000..ff8fcf4d --- /dev/null +++ b/snowboard-plugin-development.md @@ -0,0 +1,179 @@ +# Snowboard Plugin Development + +- [Introduction](#introduction) +- [Framework Concepts](#concepts) + - [The Snowboard class](#snowboard-class) + - [The PluginLoader class](#plugin-loader-class) + - [The PluginBase and Singleton abstracts](#plugin-base-singleton) + - [Global events](#global-events) + - [Mocking](#mocking) + + +## Introduction + +The Snowboard framework has been designed to be extensible and customisable for the needs of your project. To this end, the following documentation details the concepts of the framework, and how to develop your own functionality to extend or replace features within Snowboard. + + +## Framework Concepts + +Snowboard works on the concept of an encompassing application, which acts as the base of functionality and is then extended through plugins. The main method of communication and functionality is through the use of [JavaScript classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) which offer instances and extendability, and global events - a feature built into Snowboard. + +The following classes and abstracts are included in the Snowboard framework. + + +### The Snowboard class + +The Snowboard class is the representation of the application. It is the main point of adding, managing and accessing functionality within the framework, synonymous to using `jQuery` or `Vue` in your scripts. It is injected into the global JavaScript scope, and can be accessed through the `Snowboard` variable anywhere within the application after the `{% snowboard %}` tag is used. + +In addition, Snowboard is injected into all plugin classes that are used as the entry point for a plugin, and can be accessed via `this.snowboard` inside an entry plugin class. + +```js +// All these should work to use Snowboard globally +Snowboard.getPlugins(); +snowboard.getPlugins(); +window.Snowboard.getPlugins(); + +// In your plugin, Snowboard can also be accessed as a property. +class MyPlugin extends PluginBase { + myMethod() { + this.snowboard.getPlugins(); + } +} +``` + +The Snowboard class provides the following public API for use in managing plugins and calling global events: + +Method | Parameters | Description +------ | ---------- | ----------- +`addPlugin` | name(`String`)
    instance(`PluginBase`) | Adds a plugin to the Snowboard framework. The name should be a unique name, unless you intend to replace a pre-defined plugin. The instance should be either a [PluginBase or Singleton](#plugin-base-singleton) instance that represents the "entry" point to your plugin. +`removePlugin` | name(`String`) | Removes a plugin, if it exists. When a plugin is removed, all active instances of the plugin will be destroyed. +`hasPlugin` | name(`String`) | Returns `true` if a plugin with the given name has been added. +`getPlugin` | name(`String`) | Returns the [PluginLoader](#plugin-loader-class) instance for the given plugin, if it exists. If it does not exist, an error will be thrown. +`getPlugins` | | Returns an object of added plugins as [PluginLoader](#plugin-loader-class) instances, keyed by the name of the plugin. +`getPluginNames` | | Returns all added plugins by name as an array of strings. +`listensToEvent` | eventName(`String`) | Returns an array of plugin names as strings for all plugins that listen to the given event name. This works for both Promise and non-Promise [global events](#global-events). +`globalEvent` | eventName(`String`)
    *...parameters* | Calls a non-Promise [global event](#global-events). This will trigger event callbacks for all plugins listening to the given event. This method can be provided additional parameters that will be forwarded through to the event callbacks. This method returns `false` if the event was cancelled by a plugin. +`globalPromiseEvent` | eventName(`String`)
    *...parameters* | Calls a Promise [global event](#global-events). This will trigger event callbacks for all plugins listening to the given event. This method can be provided additional parameters that will be forwarded through to the event callbacks. This method returns a Promise that will either be resolved or rejected depending on the response from the event callbacks of the plugins. +`debug` | *...parameters* | When the application is in debug mode, this method logs debug messages to the console. Each log message can display one or more parameters at the same time, and includes a trace of the entire call stack up to when the debug call was made. + +#### Debugging in Snowboard + +The Snowboard class provides a `debug` method that allows developers to easily debug their Snowboard application and plugins. This method only works if the Winter application is in debug mode (`'debug' => true` in the `config/app.php` file). + +Debugging can be called anywhere that the Snowboard class is accessible. + +```js +// Globally +Snowboard.debug('This is a debug message'); + +// Within a plugin +class MyPlugin extends PluginBase { + myMethod() { + this.snowboard.debug('Debugging my plugin', this); + } +} +``` + +In general, you would use the first parameter of the `debug` method to state the debug message. From there, additional parameters can be added to provide additional context. The method will print a collapsed debug message to your developer console on your browser. You may extend the debug message in your console to view a stack trace, showing the entire call stack up to when the debug message was triggered. + + +### The PluginLoader class + +The PluginLoader class is the conduit between your application (ie. the [Snowboard class](#snowboard-class)) and the plugins. It acts similar to a "factory", providing and managing instances of the plugins and allowing the Snowboard application to communicate to those instances. It also provides a basic level of mocking, to allow for testing or overwriting individual methods of the plugin dynamically. + +Each PluginLoader instance will be representative of one plugin. + +In general, you will not need to interact with this class directly - most developer-facing functionality should be done on the Snowboard class or the plugins themselves. Thus, we will only document the methods that *may* be accessed by developers. + +Method | Parameters | Description +------ | ---------- | ----------- +`hasMethod` | methodName(`String`) | Returns `true` if the plugin defines a method by the given name. +`getInstance` | *...parameters* | Returns an instance of the plugin. Please see the **Plugin instantiation** section below for more information. +`getInstances` | | Returns all current instances of the plugin. +`getDependencies` | | Returns an array of the names of all plugins that the current plugin depends on, as strings. +`dependenciesFulfilled` | | Returns `true` if the current plugin's dependencies have been fulfilled. +`mock` | methodName(`String`)
    callback(`Function`) | Defines a mock for the current plugin, replacing the given method with the provided callback. See the [Mocking](#mocking) section for more information. +`unmock` | methodName(`String`) | Restores the original functionality for a previously-mocked method. See the [Mocking](#mocking) section for more information. + + +### The `PluginBase` and `Singleton` abstracts + +These classes are the base of all plugins in Snowboard, and represent the base functionality that each plugin contains. When creating a plugin class, you will almost always extend one of these abstract classes. + +There are two key differences between these abstracts, based on the reusability of the plugin and how it is instantiated in the course of the JavaScript functionality of your project: + +Detail | `PluginBase` | `Singleton` +------ | ------------ | ----------- +Reusability | Each use of the plugin creates a new instance of the plugin | Each use of the plugin uses the same instance. +Instantiation | Must be instantiated manually when it is needed to be used | Instantiated automatically when the page is loaded + +The reason for the separation is to provide better definition on how your plugin is intended to be used. For `PluginBase`, you would use this when you want each instance to have its own scope and data. Contrarily, you would use `Singleton` if you want the same data and scope to be shared no matter how many times you use the plugin. + +Here are some examples of when you would use one or the other: + +- `PluginBase` + - AJAX requests + - Flash messages + - Reusable widgets with their own data +- `Singleton` + - Event listeners + - Global utilities + - Base user-interface handlers + + +### Global events + +Global events are an intrinsic feature of the Snowboard framework, allowing Snowboard plugins to respond to specific events in the course of certain functionality, similar in concept to DOM events or the Event functionality of Winter CMS. + +There are two entities that are involved in any global event: + +- The **triggering** class, which fires the global event with optional extra context, and, +- The **listening** class, which listens for when a global event is fired, and actions its own functionality. + +There are also two types of global events that can be triggered, they are: + +- A **standard** event, which fires and executes all listeners in listening classes, and, +- A **Promise** event, which fires and waits for the Promise to be resolved before triggering further functionality. + +In practice, you would generally use standard events for events in where you do not necessarily want to wait for a response. In all other cases, you would use a Promise event which allows all listening classes to respond to the event in due course. + +Firing either event is done by calling either the `globalEvent` or `globalPromiseEvent` method directly on the main Snowboard class. + +```js +// Standard event +snowboard.globalEvent('myEvent', context); + +// Promise event +snowboard.globalPromiseEvent('myPromiseEvent').then( + () => { + // functionality when the promise is resolved + }, + () => { + // functionality when the promise is rejected + } +); +``` + +For a plugin to register as a listening class, it must specify a `listens` method in the plugin class that returns an object. Each key should be the global event being listened for, and the value should be the name of a method inside the class that will handle the event when fired. This is the same whether the event is a standard event or a Promise event. + +```js +class MyPlugin extends PluginBase { + listens() { + return { + ready: 'ready', + eventName: 'myHandler', + }; + } + + ready() { + // This method is run when the `ready` global event is fired. + } + + myHandler(context) { + // This method is run when the `eventName` global event is fired. + } +} +``` + +Snowboard only has one in-built global event that is fired - the `ready` event, which is fired when the DOM is loaded and the page is ready. This event is synonymous with jQuery's `ready` event, and is mainly used to instantiate the Singletons that have been registered with Snowboard. + diff --git a/snowboard-request.md b/snowboard-request.md new file mode 100644 index 00000000..714fe878 --- /dev/null +++ b/snowboard-request.md @@ -0,0 +1,368 @@ +# AJAX Requests (JavaScript API) + +- [Introduction](#introduction) +- [Request workflow](#request-workflow) +- [Available options](#available-options) +- [Global events](#global-events) +- [Element events](#element-events) +- [Usage examples](#usage-examples) +- [Extending or replacing the Request class](#extending-replacing) + + +## Introduction + +Snowboard provides core AJAX functionality via the `Request` Snowboard plugin. The `Request` plugin provides powerful flexibility and reusability in making AJAX Requests with the Backend functionality in Winter. It can be loaded by adding the following tag into your CMS Theme's page or layout: + +```twig +{% snowboard request %} +``` + +And called using the following code in your JavaScript: + +```js +Snowboard.request('#element', 'onAjax', {}); +``` + +The base `Request` plugin uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) provided in most modern browsers to execute AJAX requests from the frontend to the backend in Winter. + +>**NOTE:** If you would like to replace any part of the functionality of the base `Request` plugin then you can write a custom [Snowboard Plugin](plugin-development) that extends and overrides the base `Request` plugin to customize it as desired. + +The `request` method takes three parameters: + + +
    + +Parameter | Required | Description +--------- | -------- | ----------- +`element` | No | The element that this AJAX request is targeting, either as a `HTMLElement` instance, or as a CSS-selector string. This can be any element, but usually will be used with a `form` element. +`handler` | **Yes** | The AJAX handler to call. This should be in the format `on`. +`options` | No | The [AJAX request options](#available-options), as an object. + + +## Request workflow + +AJAX requests made through the `Request` class go through the following process: + +- The request is initialized and validated with the given element and options. +- If `browserValidate` is enabled and the AJAX request is done with a form, browser-side validation occurs. If this fails, the request is cancelled. +- If `confirm` is specified, a confirmation is presented to the user. If they cancel, the request is cancelled. +- The data provided to the Request, along with any form data if applicable, will be compiled. +- The AJAX request is sent to the Backend context using the [given handler](../snowboard/handlers). +- A response is received from the Backend context and processed, from here, one of three things happen: + - If the response is successful, then any partials that are instructed to be updated will be updated at this point. + - If the response is unsuccessful due to a validation error, then a validation message will be shown and the failing fields will be highlighted. + - If the response is unsuccessful due to any other error, then an error message will be shown. +- The request is then complete. + + +## Available options + +All options below are optional. + + +
    + +Option | Type | Description +------ | ---- | ----------- +`confirm` | `string` | If provided, the user will be prompted with this confirmation message, and will be required to confirm if they wish to proceed with the request. +`data` | `Object` | Extra data that will be sent to the server along with any form data, if available. If `files` is `true`, you may also include files in the request by using [`Blob` objects](https://developer.mozilla.org/en-US/docs/Web/API/Blob). +`redirect` | `string` | If provided, the browser will be redirected to this URL after a request is successfully completed. +`form` | `HTMLElement` or `string` | Specifies the form that data will be extracted from and sent in the request. If this is not provided, the form will be automatically determined from the element provided with the request. If no element is given, then no form will be used. +`files` | `boolean` | If `true`, this request will accept file uploads in the data. +`browserValidate` | `boolean` | If `true`, the in-built client-side validation provided by most common browsers will be performed before sending the request. This is only applied if a form is used in the request. +`flash` | `boolean` | If `true`, the request will process and display any flash messages returned in the response. +`update` | `Object` | Specifies a list of partials and page elements that can be changed through the AJAX response. The key of the object represents the partial name and the value represents the page element (as a CSS selector) to target for the update. If the selector string is prepended with an `@` symbol, the content will be appended to the target. If the selector string is prepended with a `^` symbol, it will instead be prepended to the target. +`fetchOptions` | `Object` | If specified, this will override the options used with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch) to make the request. + +The following callbacks may also be specified in the `options` parameter. All callbacks expect a function to be provided. The `this` keyword inside all callbacks will be assigned the `Request` instance that represents the current AJAX request. + +Callback | Description +-------- | ----------- +`beforeUpdate` | Executes before page elements are updated with the response data. The function receives one parameter: the response data from the AJAX response as an object. +`success` | Execures when the AJAX request is successfully responded to. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. +`error` | Executes when the AJAX request fails due to a server-side error or validation error. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. +`complete` | Executes when the AJAX request is complete, regardless of success or failure. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. + +Finally, the following option parameters define override functionality for various actions that the `Request` instance may take during the processing of a response. As with the callback methods, these must be provided as a function. + + +
    + +Option | Parameters | Description +------ | ---------- | ----------- +`handleConfirmMessage` | `(string) confirmationMessage` | Handles any confirmations requested of the user. +`handleErrorMessage` | `(string) errorMessage` | Handles any errors occuring during the request +`handleValidationMessage` | `(string) message, (Object) fieldMessages` | Handles validation errors occurring during the request. `fieldMessages` has field names as the key and messages as the value. +`handleFlashMessage` | `(string) message, (string) type` | Handles flash messages. +`handleRedirectResponse` | `(string) redirectUrl` | Handles redirect responses. + + +## Global events + +The `Request` class fires several global events which can be used by plugins to augment or override the functionality of the `Request` class. [Snowboard plugins](../snowboard/plugin-development) can be configured to listen to, and act upon, these events by using the `listen()` method to direct the event to a method with the plugin class. + +```js +class HandleFlash extends Snowboard.Singleton +{ + /** + * Defines listeners for global events. + * + * @returns {Object} + */ + listens() { + return { + // when the "ajaxFlashMessages" event is called, run the "doFlashMessage" method in this class + ajaxFlashMessages: 'doFlashMessages', + }; + } + + doFlashMessages(messages) { + Object.entries(messages).forEach((entry) => { + const [cssClass, message] = entry; + + showFlash(message, cssClass); + }); + } +} +``` + +Some events are called as Promise events, which means that your listener must itself return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) this is either resolved or rejected. + +```js +class ConfirmEverything extends Snowboard.Singleton +{ + /** + * Defines listeners for global events. + * + * @returns {Object} + */ + listens() { + return { + // when the "ajaxConfirmMessage" event is called, run the "confirm" method in this class + ajaxConfirmMessage: 'confirm', + }; + } + + // Confirm all confirmation messages + confirm() { + return Promise.resolve(true); + } +} +``` + +The following events are called during the Request process: + + +
    + +Event | Promise? | Parameters | Description +----- | -------- | ---------- | ----------- +`ajaxSetup` | No | `(Request) request` | Called after the Request is initialized and checked that it can be called. It is intended to be used for modifying the Request parameters before sending to the server. Returning `false` in any event listeners will cancel the request. +`ajaxConfirmMessage` | Yes | `(Request) request, (string) confirmationMessage` | Called if `confirm` is specified, and the Request is ready to be sent to the server. This allows developers to customise the confirmation process or display. If an event listener rejects the Promise, this will cancel the request. +`ajaxBeforeSend` | No | `(Request) request` | Called immediately before the Request is sent. It can be used for final changes to the Request, or cancelling it prematurely. Returning `false` in any event listeners will cancel the request. +`ajaxFetchOptions` | No | `(Object) fetchOptions, (Request) request` | Called immediately when the `Fetch API` is initialised to make the request. It can be used to modify the Fetch options via a plugin. This event cannot be cancelled. +`ajaxStart` | No | `(Promise) callback, (Request) request` | Called when the Request is sent. This event cannot be cancelled. +`ajaxBeforeUpdate` | Yes | `(mixed) response, (Request) request` | Called when a successful response is returned and partials are going to be updated. It can be used to determine which partials will be updated, or can also be used to cancel the partial updates. If an event listener rejects the Promise, no partials will be updated. +`ajaxUpdate` | No | `(HTMLElement) element, (string) content, (Request) request` | Called when an individual partial is updated. It can be used to make further updates to an element, or handle updates. Note that this event is fired *after* the element is updated. This event cannot be cancelled. +`ajaxUpdateComplete` | No | `(array of HTMLElement) elements, (Request) request)` | Called when the partials are updated. It can be used to determine which partials have been updated. This event cannot be cancelled. +`ajaxSuccess` | No | `(Object) responseData, (Request) request` | Called when a successful response is returned and all partial updating is completed. It can be used to cancel further response handling (ie. redirects, flash messages). Returning `false` in any event listeners will prevent any further response handling from taking place. +`ajaxError` | No | `(Object) responseData, (Request) request` | Called when an unsuccessful response is returned from the AJAX request. It can be used to cancel further error handling. Returning `false` in any event listeners will prevent any further response handling from taking place. +`ajaxRedirect` | No | `(string) redirectUrl, (Request) request` | Called when a redirect is to take place, either from the response or through the `redirect` option. Returning `false` in any event listeners will prevent the redirect from executing. +`ajaxErrorMessage` | No | `(string) message, (Request) request` | Called when an error message is to be shown to the user. Returning `false` in any event listeners will prevent the default error handling (showing an alert to the user) from executing. +`ajaxFlashMessages` | No | `(array of Object) flashMessages, (Request) request` | Called when one or more flash messages are to be shown to the user. There is no default functionality for flash messages, so if no event listeners trigger for this event, no activity will occur. +`ajaxValidationErrors` | No | `(HTMLElement) form, (array) fieldMessages, (Request) request` | Called when a validation error is returned in the response. There is no default functionality for validation errors, so if no event listeners trigger for this event, no activity will occur. + + +## Element events + +In addition to global events, local events are fired on elements that trigger an AJAX request. These events are treated as [DOM events](https://developer.mozilla.org/en-US/docs/Web/API/Event) and thus can be listened to by normal DOM event listeners or your framework of choice. The `Request` class will inject properties in the event depending on the type of event and the `event.request` property will always be the `Request` instance. + +```js +const element = document.getElementById('my-button'); +element.addEventListener('ajaxAlways', (event) => { + console.log(event.request); // The Request instance + console.log(event.responseData); // The raw response data as an object + console.log(event.responseError); // An error object if the response failed +}); +``` + +In most cases, you can cancel the event, and thus the Request, by adding `event.preventDefault()` to your callback. + +```js +const element = document.getElementById('my-button'); +element.addEventListener('ajaxSetup', (event) => { + // Never process a request for this element + event.preventDefault(); +}); +``` + +The following events are called during the Request process directly on the element that triggered the request: + + +
    + +Event | Description +----- | ----------- +`ajaxSetup` | Called after the Request is initialized and checked that it can be called. +`ajaxPromise` | Called when the AJAX request is sent to the server. A property called `promise` is provided, which is the Promise that is resolved or rejected when the response succeeds or fails. +`ajaxUpdate` | Called when an element is updated by a partial update from an AJAX response. **This event is fired on the element that is updated, not the triggering element.** and thus does not get given the `Request` instance. A property called `content` is provided with the content that was added to this element. +`ajaxDone` | Called when this element makes a successful AJAX request. A property called `responseData` is provided with the raw response data, as an object. +`ajaxFail` | Called when this element makes an unsuccessful AJAX request. A property called `responseError` is provided with the error object. +`ajaxAlways` | Called when an AJAX request is completed, regardless of success or failure. It is provided two properties: `responseData` and `responseError`, which represent the raw response data and error object, depending on whether the AJAX request succeeded or failed. + + +## Usage examples + +Make a simple request to an AJAX handler without specifying a triggering element. + +```js +Snowboard.request(null, 'onAjax'); +``` + +Request a confirmation before submitting a form, and redirect them to a success page. + +```js +Snowboard.request('form', 'onSubmit', { + confirm: 'Are you sure you wish to submit this data?', + redirect: '/form/success', +}); +``` + +Run a calculation handler and inject some data from the page, then update the total. + +```js +Snowboard.request('#calculate', 'onCalculate', { + data: { + firstValue: document.getElementById('first-value').value, + secondValue: document.getElementById('second-value').value, + }, + update: { + totalResult: '.total-result' + }, +}); +``` + +Run a calculation handler and show a success message when calculated. + +```js +Snowboard.request('#calculate', 'onCalculate', { + data: { + firstValue: document.getElementById('first-value').value, + secondValue: document.getElementById('second-value').value, + }, + success: (data) => { + const total = data.total; + alert('The answer is ' + total); + }, +}); +``` + +Prompt a user to confirm a redirect. + +```js +Snowboard.request('form', 'onSubmit', { + handleRedirectResponse: (url) => { + if (confirm('Are you sure you wish to go to this URL: ' + url)) { + window.location.assign(url); + } else { + alert('Redirect cancelled'); + } + }, +}); +``` + +Track when an element is updated from an AJAX request. + +```js +const element = document.getElementById('updated-element'); +element.addEventListener('ajaxUpdate', (event) => { + console.log('The "updated-element" event was updated with the following content:', event.content); +}); +``` + +Disable file uploads globally from AJAX requests by modifying the `Request` instance. + +```js +// In your own Snowboard plugin +class DisableFileUploads extends Snowboard.Singleton +{ + listens() { + return { + ajaxSetup: 'stopFileUploads', + }; + } + + stopFileUploads(request) { + request.options.files = false; + } +} +``` + + +## Extending or replacing the Request class + +As part of making Snowboard an extensible and flexible platform, developers have the option to extend, or replace entirely, the base Request class. This allows developers to use their own preferred platforms and frameworks for executing the AJAX functionality, partial updates, and much more. + +For example, if a developer wanted to use the [Axios library](https://github.com/axios/axios) to execute AJAX requests, as opposed to the in-built Fetch API, one could do this by creating their own Snowboard plugin and extending the `Request` class, replacing the `doAjax()` method in their own class: + +```js +const axios = require('axios'); + +class AxiosRequest extends Request +{ + doAjax() { + // Allow plugins to cancel the AJAX request before sending + if (this.snowboard.globalEvent('ajaxBeforeSend', this) === false) { + return Promise.resolve({ + cancelled: true, + }); + } + + const ajaxPromise = axios({ + method: 'post', + url: this.url, + headers: this.headers, + data: this.data, + }); + + this.snowboard.globalEvent('ajaxStart', ajaxPromise, this); + + if (this.element) { + const event = new Event('ajaxPromise'); + event.promise = ajaxPromise; + this.element.dispatchEvent(event); + } + + return ajaxPromise; + } +} +``` + +You could then either replace the `request` plugin with this class, or alias it as something else: + +```js +Snowboard.removePlugin('request'); +Snowboard.addPlugin('request', AxiosRequest); + +// Or run it as an alias +Snowboard.addPlugin('axios', AxiosRequest); +// And call it thusly +Snowboard.axios('#my-element', 'onSubmit'); +``` + +For more information on the best practices with setting up a Snowboard plugin, view the [Plugin Development](../snowboard/plugin-development) documentation. \ No newline at end of file diff --git a/snowboard-utilities.md b/snowboard-utilities.md new file mode 100644 index 00000000..ecacdbd5 --- /dev/null +++ b/snowboard-utilities.md @@ -0,0 +1,276 @@ +# Snowboard Utilities + +- [Introduction](#introduction) +- [Cookie](#cookie) + - [Basic Usage](#cookie-basic-usage) + - [Encoding](#cookie-encoding) + - [Cookie Attributes](#cookie-attributes) + - [expires](#cookie-attributes-expires) + - [path](#cookie-attributes-path) + - [domain](#cookie-attributes-domain) + - [secure](#cookie-attributes-secure) + - [sameSite](#cookie-attributes-sameSite) + - [Setting Defaults](#cookie-attributes-defaults) + - [Cookie Events](#cookie-events) + - [`cookie.get`](#cookie-event-get) + - [`cookie.set`](#cookie-event-set) +- [JSON Parser](#json-parser) +- [Sanitizer](#sanitizer) + + +## Introduction + +The Snowboard framework included several small utilities by default that help make development easier. + + +## Cookie + +The Cookie utility is a small wrapper around the [js-cookie](https://github.com/js-cookie/js-cookie/) package that provides a simple, lightweight JS API for interacting with browser cookies. + + +### Basic Usage + +Create a cookie, valid across the entire site: + +```js +Snowboard.cookie().set('name', 'value') +``` + +Create a cookie that expires 7 days from now, valid across the entire site: + +```js +Snowboard.cookie().set('name', 'value', { expires: 7 }) +``` + +Create an expiring cookie, valid to the path of the current page: + +```js +Snowboard.cookie().set('name', 'value', { expires: 7, path: '' }) +``` + +Read cookie: + +```js +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().get('nothing') // => undefined +``` + +Read all visible cookies: + +```js +Snowboard.cookie().get() // => { name: 'value' } +``` + +>**NOTE:** Cookies can only be read if the place they are being read from has access to read the cookie according to the browser. + +Delete cookie: + +```js +Snowboard.cookie().remove('name') +``` + +Delete a cookie valid to the path of the current page: + +```js +Snowboard.cookie().set('name', 'value', { path: '' }) +Snowboard.cookie().remove('name') // fail! +Snowboard.cookie().remove('name', { path: '' }) // removed! +``` + +>**IMPORTANT!** When deleting a cookie and you're not relying on the [default attributes](#cookie-attributes-defaults), you must pass the exact same path and domain attributes that were used to set the cookie + +```js +Snowboard.cookie().remove('name', { path: '', domain: '.yourdomain.com' }) +``` + +>**NOTE:** Removing a nonexistent cookie neither raises any exception nor returns any value. + + +### Encoding + +The package is [RFC 6265](http://tools.ietf.org/html/rfc6265#section-4.1.1) compliant. All special characters that are not allowed in the cookie-name or cookie-value are encoded with each one's UTF-8 Hex equivalent using [percent-encoding](http://en.wikipedia.org/wiki/Percent-encoding). +The only character in cookie-name or cookie-value that is allowed and still encoded is the percent `%` character, it is escaped in order to interpret percent input as literal. +Please note that the default encoding/decoding strategy is meant to be interoperable [only between cookies that are read/written by js-cookie](https://github.com/js-cookie/js-cookie/pull/200#discussion_r63270778). To override the default encoding/decoding strategy you need to use a [converter](#cookie-converters). + +>**NOTE:** According to [RFC 6265](https://tools.ietf.org/html/rfc6265#section-6.1), your cookies may get deleted if they are too big or there are too many cookies in the same domain, [more details here](https://github.com/js-cookie/js-cookie/wiki/Frequently-Asked-Questions#why-are-my-cookies-being-deleted). + + +### Cookie Attributes + +Cookie attributes can be set globally by creating an instance of the API via `withAttributes()`, or individually for each call to `Snowboard.cookie().set(...)` by passing a plain object as the last argument. Per-call attributes override the default attributes. + +>**NOTE:** You should never allow untrusted input to set the cookie attributes or you might be exposed to a [XSS attack](https://github.com/js-cookie/js-cookie/issues/396). + + +#### expires + +Defines when the cookie will be removed. Value must be a [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) which will be interpreted as days from time of creation or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) instance. If omitted, the cookie becomes a session cookie. + +To create a cookie that expires in less than a day, you can check the [FAQ on the Wiki](https://github.com/js-cookie/js-cookie/wiki/Frequently-Asked-Questions#expire-cookies-in-less-than-a-day). + +**Default:** Cookie is removed when the user closes the browser. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { expires: 365 }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### path + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) indicating the path where the cookie is visible. + +**Default:** `/` + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { path: '' }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name', { path: '' }) +``` + + +#### domain + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) indicating a valid domain where the cookie should be visible. The cookie will also be visible to all subdomains. + +**Default:** Cookie is visible only to the domain or subdomain of the page where the cookie was created + +**Examples:** + +Assuming a cookie that is being created on `example.com`: + +```js +Snowboard.cookie().set('name', 'value', { domain: 'subdomain.example.com' }) +Snowboard.cookie().get('name') // => undefined (need to read at 'subdomain.example.com') +``` + + +#### secure + +Either `true` or `false`, indicating if the cookie transmission requires a secure protocol (https). + +**Default:** No secure protocol requirement. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { secure: true }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### sameSite + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), allowing to control whether the browser is sending a cookie along with cross-site requests. + +Default: not set. + +>**NOTE:** More recent browsers are making "Lax" the default value even without specifiying anything here. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { sameSite: 'strict' }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### Setting up defaults + +In order to set global defaults that are used for every cookie that is created with the `Snowboard.cookie().set('name', 'value')` method you can call the `setDefaults(options)` method on the Cookie plugin and it will set the provided options as the global defaults. + +If you want to get the current defaults, call `Snowboard.cookie().getDefaults()`. + +```js +Snowboard.cookie().setDefaults({ path: '/', domain: '.example.com' }); +Snowboard.cookie().set('example', 'value'); +``` + + +### Events + +The Cookie plugin provides the ability to interact with cookies and modify their values during accessing or creating. + + +#### `cookie.get` + +This event runs during `Snowboard.cookie().get()` and provides the `(string) name` & `(string) value` parameters, along with a callback method that can be used by a plugin to override the cookie value programatically. This can be used to manipulate or decode cookie values. + +```js +class CookieDecryptor extends Singleton +{ + listens() { + return { + 'cookie.get': 'decryptCookie', + }; + } + + decryptCookie(name, value, setValue) { + if (name === 'secureCookie') { + setValue(decrypt(value)); + } + } +} +``` + + +#### `cookie.set` + +This event runs during `Snowboard.cookie().set()` and provides the `(string) name` & `(string) value` parameters, along with a callback method that can be used by a plugin to override the value saved to the cookie programatically. This will allow you to manipulate or encrypt cookie values before storing them with the browser. + +```js +class CookieEncryptor extends Singleton +{ + listens() { + return { + 'cookie.set': 'encryptCookie', + }; + } + + encryptCookie(name, value, setValue) { + if (name === 'secureCookie') { + setValue(encrypt(value)); + } + } +} +``` + + +## JSON Parser + +The JSON Parser utility is used to safely parse JSON-like (JS-object strings) data that does not strictly meet the JSON specifications. It is especially useful for parsing the values provided in the `data-request-data` attribute used by the [Data Attributes](data-attributes) functionality. + +This is somewhat similar to [JSON5](https://json5.org/) or [RJSON](http://www.relaxedjson.org/), but not exactly. The key aspect is that it allows for data represented as a JavaScript Object in string form as if it was actively running JS to be parsed without the use of `eval()` which could cause issues with Content Security Policies that block the use of `eval()`. + +>**NOTE:** Although this functionality is documented it is unlikely that regular developers will ever have need of interacting with this feature. + +### Usage: + +```js +let data = "key: value, otherKey: 'other value'; +let object = Snowboard.jsonParser().parse(`{${data}}`); +``` + + +## Sanitizer + +The Sanitizer utility is a client-side HTML sanitizer designed mostly to prevent self-XSS attacks. Such an attack could look like a user copying content from a website that uses clipboard injection to hijack the values actually stored in the clipboard and then having the user paste the content into an environment where the content would be treated as HTML, typically in richeditor / WYSIWYG fields. + +The sanitizer utility will strip all attributes that start with `on` (usually JS event handlers as attributes, i.e. `onload` or `onerror`) or that contain the `javascript:` pseudo protocol in their values. + +It is available both as a global function (`wnSanitize(html)`) and as a Snowboard plugin. + +The following example shows how the Froala WYSIWYG editor can be hooked into to protect against a clipboard injection / self-XSS attack. + +```js +$froalaEditor.on('froalaEditor.paste.beforeCleanup', function (ev, editor, clipboard_html) { + return Snowboard.sanitizer().sanitize(clipboard_html); +}); +``` \ No newline at end of file diff --git a/themes-development.md b/themes-development.md index 2e1e26b8..f1ea27ce 100644 --- a/themes-development.md +++ b/themes-development.md @@ -16,24 +16,31 @@ The theme directory could include the **theme.yaml**, **version.yaml** and **ass The theme information file **theme.yaml** contains the theme description, the author name, URL of the author's website and some other information. The file should be placed to the theme root directory: -``` +```css πŸ“‚ themes - ┣ πŸ“‚ example-theme - ┃ β”— πŸ“œ theme.yaml <-- Theme information file + β”— πŸ“‚ example-theme + β”— πŸ“œ theme.yaml <-- Theme information file ``` The following fields are supported in the **theme.yaml** file: + +
    + Field | Description ------------- | ------------- -**name** | specifies the theme name, required. -**author** | specifies the author name, required. -**homepage** | specifies the author website URL, required. -**description** | the theme description, required. -**previewImage** | custom preview image, path relative to the theme directory, eg: `assets/images/preview.png`, optional. -**code** | the theme code, optional. The value is used on the Winter CMS marketplace for initializing the theme code value. If the theme code is not provided, the theme directory name will be used as a code. When a theme is installed from the Marketplace, the code is used as the new theme directory name. -**form** | a configuration array or reference to a form field definition file, used for [theme customization](#customization), optional. -**require** | an array of plugin names used for [theme dependencies](#dependencies), optional. +`name` | specifies the theme name, required. +`author` | specifies the author name, required. +`homepage` | specifies the author website URL, required. +`description` | the theme description, required. +`previewImage` | custom preview image, path relative to the theme directory, eg: `assets/images/preview.png`, optional. +`code` | the theme code, optional. The value is used on the Winter CMS marketplace for initializing the theme code value. If the theme code is not provided, the theme directory name will be used as a code. When a theme is installed from the Marketplace, the code is used as the new theme directory name. +`form` | a configuration array or reference to a form field definition file, used for [theme customization](#customization), optional. +`require` | an array of plugin names used for [theme dependencies](#dependencies), optional. +`mix` | an object that defines Mix packages contained in your theme for [asset compilation](../console/asset-compilation). Example of the theme information file: @@ -50,10 +57,10 @@ code: "demo" The theme version file **version.yaml** defines the current theme version and the change log. The file should be placed to the theme root directory: -``` +```css πŸ“‚ themes - ┣ πŸ“‚ example-theme - ┃ β”— πŸ“œ version.yaml <-- Theme version file + β”— πŸ“‚ example-theme + β”— πŸ“œ version.yaml <-- Theme version file ``` The file format is following: @@ -69,12 +76,12 @@ The file format is following: The theme preview image is used in the backend theme selector. The image file **theme-preview.png** should be placed to the theme's **assets/images** directory: -``` +```css πŸ“‚ themes - ┣ πŸ“‚ example-theme - ┃ ┣ πŸ“‚ assets - ┃ ┃ ┣ πŸ“‚ images - ┃ ┃ ┃ β”— πŸ“œ theme-preview.png <-- Theme Preview Image + β”— πŸ“‚ example-theme + β”— πŸ“‚ assets + β”— πŸ“‚ images + β”— πŸ“œ theme-preview.png <-- Theme Preview Image ``` The image width should be at least 600px. The ideal aspect ratio is 1.5, for example 600x400px. @@ -162,7 +169,7 @@ a { color: @link-color } ## Theme dependencies -A theme can depend on plugins by defining a **require** option in the [Theme information file](#theme-information), the option should supply an array of plugin names that are considered requirements. A theme that depends on **Acme.Blog** and **Acme.User** can define this requirement like so: +A theme can depend on plugins by defining a `require` option in the [Theme information file](#theme-information), the option should supply an array of plugin names that are considered requirements. A theme that depends on **Acme.Blog** and **Acme.User** can define this requirement like so: ```yaml name: "Winter CMS Demo" @@ -187,14 +194,14 @@ Themes can provide backend localization keys through files placed in the **lang* Below is an example of the theme's lang directory: -``` -πŸ“¦themes - ┣ πŸ“‚ example-theme <-- Theme directory - ┃ ┣ πŸ“‚ lang <-- Localization directory - ┃ ┃ ┣ πŸ“‚ en <-- Specific locale directory - ┃ ┃ ┃ β”— πŸ“œ lang.php <-- Localization file - ┃ ┃ ┣ πŸ“‚ fr - ┃ ┃ ┃ β”— πŸ“œ lang.php +```css +πŸ“‚ themes + β”— πŸ“‚ example-theme <-- Theme directory + β”— πŸ“‚ lang <-- Localization directory + ┣ πŸ“‚ en <-- Specific locale directory + ┃ β”— πŸ“œ lang.php <-- Localization file + β”— πŸ“‚ fr + β”— πŸ“œ lang.php ``` The **lang.php** file should define and return an array of any depth, for example: