simplon / core
Strongly opinionated MVC framework for rapid development
Installs: 5 357
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 2
Open Issues: 0
Requires
- php: >=7.2
- filp/whoops: ^2.1
- mnapoli/silly: 1.6.*
- relay/relay: ^1.1
- simplon/device: ^0.3
- simplon/form: ^2.5
- simplon/helper: ^1.0
- simplon/http: ^0.0
- simplon/interfaces: ^0.0
- simplon/locale: ^0.4
- simplon/template: ^0.8
- simplon/url: ^0.6
- spatie/array-to-xml: ^2.7
- zendframework/zend-diactoros: ^2.2
- dev-master
- 0.9.9
- 0.9.8
- 0.9.7
- 0.9.6
- 0.9.5
- 0.9.4
- 0.9.3
- 0.9.2
- 0.9.1
- 0.9.0
- 0.8.0
- 0.7.1
- 0.7.0
- 0.6.11
- 0.6.10
- 0.6.9
- 0.6.8
- 0.6.7
- 0.6.6
- 0.6.5
- 0.6.4
- 0.6.3
- 0.6.2
- 0.6.1
- 0.6.0
- 0.5.20
- 0.5.19
- 0.5.18
- 0.5.17
- 0.5.16
- 0.5.15
- 0.5.14
- 0.5.13
- 0.5.12
- 0.5.11
- 0.5.10
- 0.5.9
- 0.5.8
- 0.5.7
- 0.5.6
- 0.5.5
- 0.5.4
- 0.5.3
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.9
- 0.4.8
- 0.4.7
- 0.4.6
- 0.4.5
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.15
- 0.3.14
- 0.3.13
- 0.3.12
- 0.3.11
- 0.3.10
- 0.3.9
- 0.3.8
- 0.3.7
- 0.3.6
- 0.3.5
- 0.3.4
- 0.3.3
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.25
- 0.2.24
- 0.2.23
- 0.2.22
- 0.2.21
- 0.2.20
- 0.2.19
- 0.2.18
- 0.2.17
- 0.2.16
- 0.2.15
- 0.2.14
- 0.2.13
- 0.2.12
- 0.2.11
- 0.2.10
- 0.2.9
- 0.2.8
- 0.2.7
- 0.2.6
- 0.2.5
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.25
- 0.1.24
- 0.1.23
- 0.1.22
- 0.1.21
- 0.1.20
- 0.1.19
- 0.1.18
- 0.1.17
- 0.1.16
- 0.1.15
- 0.1.14
- 0.1.13
- 0.1.12
- 0.1.11
- 0.1.10
- 0.1.9
- 0.1.8
- 0.1.7
- 0.1.6
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- 0.0.51
- 0.0.50
- 0.0.49
- 0.0.48
- 0.0.47
- 0.0.46
- 0.0.45
- 0.0.44
- 0.0.43
- 0.0.42
- 0.0.41
- 0.0.40
- 0.0.39
- 0.0.38
- 0.0.37
- 0.0.36
- 0.0.35
- 0.0.34
- 0.0.33
- 0.0.32
- 0.0.31
- 0.0.30
- 0.0.29
- 0.0.29b
- 0.0.28
- 0.0.27
- 0.0.26
- 0.0.25
- 0.0.24
- 0.0.23
- 0.0.22
- 0.0.21
- 0.0.20
- 0.0.19
- 0.0.18
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.12
- 0.0.11
- 0.0.10
- 0.0.9
- 0.0.8
- 0.0.7
- 0.0.6
- 0.0.5
- 0.0.4
- 0.0.3
- 0.0.2
- 0.0.1
- dev-feature/improved_error_logging
- dev-symfony-4-block
- dev-legacy
- dev-fix-asset-codes
This package is auto-updated.
Last update: 2024-10-05 19:27:40 UTC
README
_ _ ___(_)_ __ ___ _ __ | | ___ _ __ ___ ___ _ __ ___ / __| | '_ ` _ \| '_ \| |/ _ \| '_ \ / __/ _ \| '__/ _ \ \__ \ | | | | | | |_) | | (_) | | | | | (_| (_) | | | __/ |___/_|_| |_| |_| .__/|_|\___/|_| |_| \___\___/|_| \___| |_|
Simplon/Core
The simplon/core package is a strongly opinionated set of libraries which forms the core of a component based app. The package requires PHP7.1+ and is built against PSR-7 in conjunction with middleware layers.
- App structure
1.1 Registry
1.2 Context
1.3 Routes
1.4 Storage
1.5 Outgoing requests - Skeletons
2.1 Generate a default app
2.2 Add a component
2.3 Add a view to a component
2.4 Add a store to a component - Middleware
3.1 Exception
3.2 Locale
3.3 Route
3.4 Auth - Controllers
4.1 ViewController
4.2 RestController - Views
5.1 Templates
5.2 Building pages - Form helper
6.1. Define form fields
6.2. Create form view
6.3. Implement in main view
6.4. Controller implementation
1. App structure
An app is mainly made up by some app-wide classes but mainly by its components which are tiny apps in itself.
The general idea is that the app will be easier to communicate and to maintain if its broken down in smaller
pieces hence the focus on components. Further, the app works with inheritence when it comes to the modules of
config
and locale
. Inheritence goes from app-level to the component so that the component has access to
app-wide data.
The main concept is MVC
which means that your controller
will receive a request, process the data with the
help of whatever is needed and pass it
on to a view or redirect to another resource. Simplon\Core distinguishes between two types of controller:
ViewController
is the first type which is ment to handle requests which should result in a website. The second
type is a RestController
which is ment to handle requests for any api related data. Latter will respond with
JSON structured
data.
Components can only communicate via events
. They can either pull
information by other component's offers
or subscribe
to events. Components describe their events in their own events class. Offers
or Subscriptions
are defined as class constants.
It's important to note that all requests will run through a number of pre-defined Middleware
with the
RouteMiddleware
as the only required one since it handles all incoming requests.
1.1. Registry
Each component needs to be registered via its own registry class. It requires a method for receiving
the Context
class and can take optionally methods for referencing Routes
, Authentication rules
and
Events
definition.
1.2. Context
App and components have Context
classes which hold all essential instances. Essential means instances which
are shared among different classes within the app respectively the component. For example, if you have a storage
for a component you would put the instance creation of that storage in the components context class. Context
classes hold also references to your config- and locale-data.
1.3. Routes
All component related routes are defined in a component based Route
class. This class holds all route
patterns
and static methods which are used to build corresponding routes.
1.4. Storage
Storage is handled via CRUD
classes. Only MySQL
adapter is available at the moment. A storage is
described by its storage- and model class. If you want to interact with your data you should go through the
storage class and avoid direct access.
1.5. Outgoing requests
Component related outgoing requests
are all collected within its own Requests
class, mainly to aid
transparency and structure. This class is obviously only needed if you have any type of these requests.
2. Skeletons
Core has a command line tool which lets your create code skeletons in order to help you setting up your app,
components or part of your component such as CrudStore
/CrudModel
classes.
You can find all possible commands by running the following command from your terminal after you installed
simplon/core
with composer install
:
vendor/bin/core -h
2.1. Generate a default app
Let's create a default app called MyApp
. We want to use Views
for our app so we will use the option --with-view
.
This is not needed if you only want to use a REST
interface.
vendor/bin/core init MyApp --with-view
2.2. Add a component
Since core is component based we need to have at least one component. Let's add one and name it Cars
.
Again, we wanna use Views
so we have to add that option but this time we have to add the name of our first ViewController
.
vendor/bin/core component Cars --with-view=Car
There is also an option for a REST
interface:
vendor/bin/core component Cars --with-rest
It's also possible to combine both options:
vendor/bin/core component Cars --with-view=Car --with-rest
2.3. Add a view to a component
If any of your components needs another View
you can run the following command which will add a new ViewController
and a default set of a View
and a Template
.
For the following example let's assume that we have a component called Team
. Now we want to add a skeleton view for resources:
vendor/bin/core view Team Resources
This should create the following files:
App/Components/Team/Controllers/TeamViewController.php
App/Components/Team/Views/Resources/ResourcesView.php
App/Components/Team/Views/Resources/ResourcesTemplate.phtml
You still need to add a Route
and register this route within the Registry
.
2.4. Add a store to a component
If any of your components needs a CrudStore
you can run the following command which will build
a default set of a store/model class. You can add the options for setting the names of the store
, model
and database table
. By default it will derive it from the component name
.
vendor/bin/core store Cars
3. Middleware
Middleware helps us to handle/structure our request/response processing. It is also some sort of simplification by pre-processing the request e.g. for authentication reasons before it hits the actual controller. Lastly, it's a great tool due to its scalability and flexiblity. So far there are four classes which come with the core:
3.1. Exception
Wraps all following processing and handles exceptions with Whoops.
3.2. Locale
If integrated it will detect a two-letter defined locale within the requesting route (e.g. /en/) or even a region specific
locale with additional three-letters (e.g. /en-us/). If latter is the case the region specific file (e.g. en-us-locale.php
)
will inherit from the main locale (e.g. en-locale.php
). This middleware expects an array of accepted locale codes
(e.g. ['en', 'de']
) but will fallback to ['en']
if non is given.
3.3. Route
The RouteMiddleware will check the requested route against all defined routes (only registered components).
It will also kick-off the event handling
for these components.
3.4. Auth
AuthMiddleware aids authentication against certain routes
, user roles
and temporary tokens
. To make this work
we need to create an AuthContainer
which is required for the AuthMiddleware
as is the ComponentsCollection
.
The later is required since all authentication rules are defined by each component.
AuthContainer
is a class you have to setup which should be extended from the core's abstact AuthContainer
class.
The AuthMiddleware will use this container to authenticate the current request by calling fetchUser
which should handle
the actual authentication. Hence, you are free to choose how you want to authenticate the request.
The AuthMiddlware will call onSuccess
or onError
callbacks if available. Both callbacks will receive a
ResponseInterface
object while onSuccess
will receive AuthUserInterface
as second parameter. Make sure to return
the ResponseInterface
object for both cases.
Eventually, you need to knit everything together. Following a rough example of your bootstrap:
$appContext = new AppContext(); $components = new ComponentsCollection(); $components->add(new FooRegistry($appContext()); $authContainer = new AuthContainer(); $middleware = new MiddlewareCollection(); $middleware->add(new AuthMiddleware($authContainer, $components)); (new Core())->run($components, $middleware);
3.4.1. Example session based AuthContainer
This is just a rough example to clarify discussed content:
namespace App\Components\Auth\Managers; use App\Components\Auth\AuthRoutes; use App\Components\Auth\Data\AuthSessionUser; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Simplon\Core\Interfaces\AuthUserInterface; use Simplon\Core\Middleware\Auth\AuthContainer; /** * @package App\Components\Auth\Managers */ class AuthViewContainer extends AuthContainer { /** * @param ServerRequestInterface $request * * @return null|AuthUserInterface */ public function fetchUser(ServerRequestInterface $request): ?AuthUserInterface { if (!empty($_SESSION['session'])) { return new AuthSessionUser($_SESSION['session']); } return null; } /** * @return callable|null */ protected function getOnError(): ?callable { return function (ResponseInterface $response) { if (empty($response->getHeaderLine('Location'))) { $response = $response->withAddedHeader('Location', AuthRoutes::toSignIn()); } return $response; }; } }
3.4.2. Example REST based AuthContainer
Here is a rough example with a bearer token
and a lookup in a user database:
namespace App\Components\Auth\Managers; use App\Components\Auth\AuthRoutes; use App\Components\Auth\Data\AuthRestUser; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Simplon\Core\Interfaces\AuthUserInterface; use Simplon\Core\Middleware\Auth\AuthContainer; use Simplon\Mysql\Mysql; use Simplon\Mysql\MysqlException; /** * @package App\Components\Auth\Managers */ class AuthRestContainer extends AuthContainer { /** * @var Mysql */ private $mysql; /** * @param Mysql $mysql */ public function __construct(Mysql $mysql) { $this->mysql = $mysql; } /** * @param ServerRequestInterface $request * * @return null|AuthUserInterface * @throws MysqlException */ public function fetchUser(ServerRequestInterface $request): ?AuthUserInterface { if ($bearer = $this->fetchAuthBearer($request)) { list($token, $secret) = explode(':', $bearer); $query = ' -- noinspection SqlDialectInspection select * from ' . AuthStore::TABLE_NAME . ' where ' . AuthModel::COLUMN_TOKEN . ' = :token '; if ($row = $this->mysql->fetchRow($query, ['token' => $token])) { $authUser = new AuthRestUser($row); if ($secret) { $authUser->validateSecret($secret); } return $authUser; } } return null; } /** * @return callable|null */ protected function getOnError(): ?callable { return function (ResponseInterface $response) { if (empty($response->getHeaderLine('Location'))) { $response = $response->withAddedHeader('Location', AuthRoutes::toSignIn()); } return $response; }; } }
4. Controllers
Each identified route ends up in a controller. There are two types of controllers which only differ in a couple of media related methods
and their response content type
. Both types expect an __invoke
method which receives either an empty array or a set of possible params. These params are partial structures of your defined route which leads to the connected controller. For instance, a route such as /some/{foo}/stuff
would match a requested route of /some/more/stuff
. For that example your controller params would hold ['foo' => 'more']
.
4.1. ViewController
This controller type is used for all requests which result in a rendered html page.
class SomeViewController extends ViewController { /** * @param array $params * * @return ResponseViewData */ public function __invoke(array $params): ResponseViewData { // some code // // you can handle redirects // if($shouldRedirect) { $this->getFlashMessage()->setFlashSuccess('Some flash message'); return $this->redirect('/some/route'); } // // or respond with view data // return $this->respond(new SomeView()); } /** * @return SomeRegistry */ public function getRegistry(): SomeRegistry { return $this->registry; } }
4.2. RestController
class SomeRestController extends RestController { /** * @param array $params * * @return ResponseViewData */ public function __invoke(array $params): ResponseRestData { // some code // // respond with array data which will be // transformed into JSON // return $this->respond(['foo' => 'bar']); } /** * @return SomeRegistry */ public function getRegistry(): SomeRegistry { return $this->registry; } }
5. Views
Views only know about the stuff they receive as dependency. The first dependency, and only requirement,
is CoreViewData
which holds the instances of Locale
, FlashMessages
and Device
. The function of Locale
is clear. FlashMessages
show messages such as warnings, errors or successes which have been defined in
the controller. Device
is used to detect defined templates based on mobile
, tablet
or anything else
.
A view expects a template and optional some data which will be injected into the template. The above mentioned
instances from our CoreViewData
are automatically injected.
5.1. Templates
As already mentioned each template receives three variables by default: $locale
, $flash
and $device
. The
view class offers also a couple of static helper methods such as View::renderWidget
which aids the need of
rendering smaller template pieces or as we call it widgets
. Here is a small example of a template:
/** * @var FlashMessage $flash * @var Locale $locale * @var Device $device * * @var string $content */ use Simplon\Core\Views\FlashMessage; use Simplon\Core\Views\View; use Simplon\Device\Device; use Simplon\Locale\Locale; ?> <?php if ($flash->hasFlash()): ?> <?= $flash->getFlash('huge') ?> <?php endif ?> <div> Some content </div> <div> <?= View::renderWidget(__DIR__ . '/SomeWidget.phtml', ['foo' => 'bar']) ?> </div>
Using device templates
By default the view uses the defined template. However, it also tries to detect device-related templates by looking for these specific templates.
For instance, let's assume that our default template is DefaultTemplate.phtml
and that we are using a tablet device
. In that case our view would look for an existing DefaultTemplateTablet.phtml
.
If such a template exists it would prefer it over the defined one. Same accounts for mobile devices
. In that case our view would look for DefaultTemplateMobile.phtml
.
Side note: a tablet device would also prefer a mobile template
in case that a tablet template
is absent.
5.2. Building pages
Building pages is quite an important piece since we nest our views: a component has its own view but owns probably
also a couple of sub-views. These sub-views will be implemented
within the component views template
as injected variable.
The component view itself will be then implemented within an app view
or maybe a session wrapper view
. The principle
is always the same: all lower views wrap the upper views and its up to you how many levels you use.
This process will be handled in the controllers of our component:
protected function buildPage(ViewInterface $view, ComponentViewData $componentViewData, GlobalViewData $globalViewData): ViewInterface { $appContext = $this->getContext()->getAppContext(); $componentView = new AccountsPageView($this->getCoreViewData(), $componentViewData); $componentView->implements($view, 'content'); $sessionView = new SessionPageView($this->getCoreViewData(), $appContext->getUserSessionManager()->read()); $sessionView->implements($componentView, 'content'); $appView = $appContext->getAppPageView($this->getCoreViewData(), $globalViewData); $appView->implements($sessionView, 'content'); return $appView; }
You can also see two data classes: ComponentViewData
and GlobalViewData
. These are helpers to transpart data to our
component- and app-views since its possible that we have many sub-views within our components. It helps us to structure
and describe our data.
6. Form helper
The core offers a couple of form helper classes to ease and structure the use within an app. The following paragraphs will show a fulll example of how to use these helpers.
6.1. Define form fields
namespace App; use Simplon\Core\Utils\Form\BaseForm; use Simplon\Form\Data\FormField; use Simplon\Form\Data\Rules\RequiredRule; use Simplon\Form\Data\Rules\EmailRule; class CreateForm extends BaseForm { const NAME = 'name'; const EMAIL = 'email'; /** * @return FormField[] */ protected function buildFields(): array { return [ $this->getName(), $this->getEmail(), ]; } /** * @return FormField */ private function getName(): FormField { return (new FormField(self::NAME))->addRule(new RequiredRule()); } /** * @return FormField */ private function getEmail(): FormField { return (new FormField(self::EMAIL))->addRule(new EmailRule()); } }
6.2. Create form view
namespace App; use App\CreateFormFields; use Simplon\Core\Utils\Form\BaseFormView; use Simplon\Form\FormError; use Simplon\Form\View\Elements\DropDownElement; use Simplon\Form\View\Elements\InputTextElement; use Simplon\Form\View\FormViewBlock; use Simplon\Form\View\FormViewRow; class CreateFormView extends BaseFormView { /** * @return FormViewBlock[] * @throws FormError */ protected function getBlocks(): array { return [ $this->buildFormViewBlock(self::BLOCK_DEFAULT) ->addRow( $this->>buildFormViewRow() ->autoColumns($this->getNameElement()) ->autoColumns($this->getEmailElement()) ), ]; } /** * @return string */ protected function getSubmitLabel(): string { return $this->getLocale()->get('form-create-submit-label'); } /** * @return InputTextElement * @throws FormError */ private function getNameElement(): InputTextElement { $element = new InputTextElement($this->getFields()->get(CreateFormFields::NAME)); $element ->setLabel($this->getLocale()->get('form-create-name-label')) ->setPlaceholder($this->getLocale()->get('form-create-name-placeholder')) ; return $element; } /** * @return InputTextElement * @throws FormError */ private function getEmailElement(): InputTextElement { $element = new InputTextElement($this->getFields()->get(CreateFormFields::EMAIL)); $element ->setLabel($this->getLocale()->get('form-create-email-label')) ->setPlaceholder($this->getLocale()->get('form-create-email-placeholder')) ; return $element; } }
6.3. Implement in main view
namespace App; use Simplon\Core\Utils\Form\ViewWithForm; class CreateView extends ViewWithForm { /** * @return string */ protected function getTemplate(): string { return __DIR__ . '/CreateTemplate.phtml'; } }
6.3.1. Main template
/** * @var Locale $locale * @var FlashMessage $flash * @var Device $device * * @var FormView $formView */ use App\AppContext; use Simplon\Core\Views\FlashMessage; use Simplon\Core\Views\View; use Simplon\Device\Device; use Simplon\Form\View\FormView; use Simplon\Locale\Locale; ?> <div class="ui grid"> <div class="sixteen wide column"> <div class="section-content"> <?= $formView->render(__DIR__ . '/FormTemplate.phtml', ['locale' => $locale]) ?> </div> </div> </div>
6.3.2. Form template
/** * @var Locale $locale * @var FormView $formView */ use App\CreateFormView; use Simplon\Form\View\FormView; use Simplon\Locale\Locale; ?> <div class="ui basic segment"> <?= $formView->getBlock(CreateFormView::BLOCK_DEFAULT)->render() ?> </div> <?= $formView->getSubmitElement()->renderElement() ?>
6.4. Controller implementation
namespace App; use App\CreateForm; use App\CreateFormView; use App\CreateView; use Simplon\Core\Controllers\ViewController; use Simplon\Core\Utils\Form\FormWrapper; use Simplon\Core\Data\ResponseViewData; use Simplon\Form\FormFields; class CreateViewController extends ViewController { /** * @param array $params * * @return ResponseViewData * @throws \Simplon\Form\FormError */ public function __invoke(array $params): ResponseViewData { $formWrapper = $this->buildFormWrapper( new CreateForm($this->getLocale()) ); if ($formWrapper->getValidator()->validate()->isValid()) { // do something with the form data return $this->redirect('/some/other/url'); } $formView = new CreateFormView($this->getLocale(), $formWrapper->getFields()); return $this->respond( new CreateView($this->getCoreViewData(), $formView) ); } }