icanboogie / operation
Feature rich controllers dedicated to a single task
Installs: 3 330
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: >=7.2
- icanboogie/bind-routing: ^5.0
- icanboogie/errors: ^2.0
Requires (Dev)
- icanboogie/icanboogie: ^5.0
- icanboogie/module: ^5.0
- phpunit/phpunit: ^8.5
README
Operations are feature rich controllers dedicated to a single task, which often is to create/update/delete records.
Preamble
Events in this document are often referenced as ICanBoogie\Operation::<event_type>
, where
<event_type>
is the type of the event. For instance, ICanBoogie\Operation::rescue
is an event
of type rescue
, fired on an instance of ICanBoogie\Operation
. Now consider a SaveOperation
class inheriting from ICanBoogie\Operation
. The rescue
event
could also be fired on one of its instances, and an event hook could be attached to
SaveOperation::rescue
to rescue the operation.
Because ICanBoogie's event system is based on class hierarchy an event hook attached to
SaveOperation::rescue
is only invoked to rescue instances of SaveOperation
and its subclasses,
whereas an event hook attached to ICanBoogie\Operation::rescue
is invoked to rescue instances of
ICanBoogie\Operation
and its subclasses, including SaveOperation
.
Thus, when you see ICanBoogie\Operation::rescue
read "the event type 'rescue' fired on
an instance of the ICanBoogie\Operation subclass I want to listen to".
Please read the documentation of the icanboogie/event package for more details about the event system.
Operation
An instance of Operation represents an operation. Although the class provides many control methods and getters, the validation and processing of the operation must be implemented by subclasses, according to their design.
Controlling the operation
Before the operation can be validated and processed, controls are ran. The controls to run are defined by the operation. The following controls are implemented and can be extended:
CONTROL_AUTHENTICATION
: Controls the authentication of the user.CONTROL_PERMISSION
: Controls the permission of the user regarding the operation.CONTROL_RECORD
: Controls the record associated with the operation. The getterrecord
is used to retrieve the record.CONTROL_OWNERSHIP
: Controls the ownership of the record by the user.CONTROL_FORM
: Controls the form associated with the operation. The getterform
is used to retrieve the form. FormNotFound can be thrown if the form associated with the operation cannot be found. FormHasExpired can be thrown to indicate that the form associated with the operation has expired.
The controls definition is obtained though the controls
magic property:
<?php use ICanBoogie\Module; use ICanBoogie\Operation; class SaveOperation extends Operation { protected function get_controls() { return [ self::CONTROL_PERMISSION => Module::PERMISSION_CREATE, self::CONTROL_RECORD => true, self::CONTROL_OWNERSHIP => true, self::CONTROL_FORM => true ] + parent::get_controls(); } }
The following events are fired during the process:
-
Before the control of the operation by the
control()
method, the eventICanBoogie\Operation::control:before
of class BeforeControlEvent is fired. Third parties may use this event to alter the controls to run, or clear them altogether. -
The event
ICanBoogie\Operation::control
of class ControlEvent is fired after the control. Third parties may use this event to alter the outcome of the control. -
On failure the event
ICanBoogie\Operation::failure
of class FailureEvent is fired, with itstype
property set tocontrol
. -
The event
ICanBoogie\Operation::get_form
of class GetFormEvent is fired if theform
getter is not overridden. It allows third parties to provide a form to check to parameters of the request.
Validating the operation
The operation needs to be validated before it is processed. The validate()
method is invoked to
validate the operation. Errors should be collected in the provided $errors
collection. The
validation is considered failed if the method returns an empty value or errors are defined.
The following events are fired during the process:
-
Before the validation the event
ICanBoogie\Operation::validate:before
of class BeforeValidateEvent is fired. Third parties may use this event to alter the errors or the status of the validation. -
After the validation the event
ICanBoogie\Operation::validate
of class ValidateEvent is fired. Third parties may use this event to alter the errors or the outcome of the validation. -
On failure the event
ICanBoogie\Operation::failure
of class FailureEvent is fired.
Processing the operation
After the control and the validation, the operation is finally processed by invoking its
process()
method. The processing of the operation is considered failed if the method returns
null
or errors are defined.
The following events are fired during the process:
-
Before the processing, the event
ICanBoogie\Operation::process:before
of class BeforeProcessEvent is fired. Third parties may use this event to alter the request, response or errors. -
After the processing, the event
ICanBoogie\Operation::process
of class ProcessEvent is fired. Third parties may use this event to alter the result, request or response.
Handling failure
Exceptions thrown during the process (control/validation/processing) are caught and turned into
Failure exceptions. The original exception is accessible using the getPrevious()
method or
the previous
property. The response of the operation is updated with the exception code and
message.
A Failure exception is also thrown in the response has a client or server error, in which case the exception is fired without a previous exception.
Note: Failed operations may be rescued by the dispatcher.
Forwarded operations
An operation is considered forwarded when the actual destination and operation name is defined using the request parameters Operation::DESTINATION and Operation::NAME. The URL of the request is irrelevant to forwarded operations, moreover whether they succeed or fail the dispatch process simply continues. For instance, this allows forms to be posted to their own view URL (not the URL of the operation) and displayed again if an error occurs.
Note: Successful responses with a
location
are NOT discarded, they will redirect the request.
Note: This feature is currently a foundation for the icanboogie/module package and its really that package who handles forwarded operations. This may change is the future.
<?php use ICanBoogie\HTTP\Request; use ICanBoogie\Operation; $request = Request::from([ 'path' => '/', 'request_params' => [ Operation::DESTINATION => 'form', Operation::NAME => 'post', // … ] ]); $operation = new SaveOperation; $response = $operation($request); $operation->is_forwarded; // true
Response
The response of the operation is represented by a Response instance. The value returned by
the process()
method is set to its rc
property. The operation is considered failed if its
value is null
, in which case the status of the operation is set to "400 Operation failed".
Response location
The Location
header is used to ask the browser to load a different web page. This is often
used to redirect the user when an operation has been performed e.g. creating/deleting a
resource. The location
property of the response is used to set that header.
Response location and XHR
Redirecting a XHR is not a desirable behavior because although we might want to redirect the user,
we still need to get the result of our request first. In that case, the value of the location
property is moved to the redirect_to
field and the location
property is set to null
.
Thus, the browser redirection is disabled, the response is returned and it's up to the developer
to choose if he should honor the redirection or not.
Dispatcher
The package provides an HTTP dispatcher to dispatch operations. It should be placed at the top of
the dispatcher chain, before any routing. The dispatcher tries to create an Operation
instance
from the specified request, and returns immediately if it fails.
Handling of the operation response
The dispatcher discards responses from forwarded operations unless the request is an XHR or the response has a location. Remember that failed operations throw a Failure exception, which can be rescued.
Rescuing failed operations
If an exception is thrown during the dispatch of the operation, the dispatcher tries to rescue it using the following steps:
- The
ICanBoogie\Operation::rescue
event of class RescueEvent is fired. Event hooks attached to this event may replace the exception or provide a response. If a response is provided it is returned. - Otherwise, if the exception is not an instance of Failure the exception is re-thrown.
- Otherwise, if the request is an XHR the response of the operation is returned.
- Otherwise, if the operation was forwarded the exception message is logged as an error and the method returns.
- Otherwise, the exception is re-thrown.
In summary, a failed operation is rescued if a response is provided during the
ICanBoogie\Operation::rescue
event, or later if the request is an XHR. Although the rescue of an
operation might be successful, the returned response can be an error response.
Note: If the operation is forwarded and the operation could not be rescued the request dispatching process will simply continue.
Defining operations
Defining operations as routes
Because operations are controllers, they may be defined in the same fashion.
The following example demonstrates how the module Nodes defines routes to set/unset the
is_online
property:
<?php namespace Icybee\Modules\Nodes\Operation; use ICanBoogie\HTTP\Request; use ICanBoogie\Operation; return [ 'api:nodes/online' => [ 'pattern' => '/api/:constructor/<nid:\d+>/is_online', 'controller' => OnlineOperation::class, 'via' => Request::METHOD_PUT, 'param_translation_list' => [ 'constructor' => Operation::DESTINATION, 'nid' => Operation::KEY ] ], 'api:nodes/offline' => [ 'pattern' => '/api/:constructor/<nid:\d+>/is_online', 'controller' => OfflineOperation::class, 'via' => Request::METHOD_DELETE, 'param_translation_list' => [ 'constructor' => Operation::DESTINATION, 'nid' => Operation::KEY ] ] ];
The class of the operation is defined as the controller of the route. Notice how the request method is used for the same route to distinguish the operation type.
The param_translation_list
array is used to define how params captured from the pathinfo
should be translated before the operation is executed. This handy feature allow routes to be
formatted from records, while providing mapping to operation key features.
<?php $node = $app->models['nodes']->one; $path = $app->url_for('api:nodes/online', $node);
Exceptions
The exception classes defined by the package implement the ICanBoogie\Operation\Exception
interface so that they can easily be identified:
<?php try { // … } catch (\ICanBoogie\Operation\Exception $e) { // an Operation exception } catch (\Throwable $e) { // some other exception }
The following exceptions are defined:
- Failure: Exception raised when an operation fails.
- FormHasExpired: Exception thrown when the form associated with an operation has expired.
- FormNotFound: Exception thrown when the form associated with the operation cannot be found.
Requirements
The package requires PHP 7.2 or later.
Installation
composer require icanboogie/operation
Documentation
The package is documented as part of the [ICanBoogie][] framework
documentation. You can generate the documentation for the package and its dependencies
with the make doc
command. The documentation is generated in the build/docs
directory.
ApiGen is required. The directory can later be cleaned with the
make clean
command.
Testing
Run make test-container
to create and log into the test container, then run make test
to run the
test suite. Alternatively, run make test-coverage
to run the test suite with test coverage. Open
build/coverage/index.html
to see the breakdown of the code coverage.
License
icanboogie/operation is released under the New BSD License.