The diagram below shows what this bundle aims to implement by using Symfony and API Platform
This structure will allow a developer to create an API providing UI structure and component resources from a database, thereby allowing a front-end application to display and manipulate the resources.
It also includes the ability to have components where files can be uploaded, handling of forms (serialization, validation and successful submissions) and a built-in component to display a collection of resources with filtering and pagination (this is more of a proxy component to allow collections of component of dynamic page resources to be included within a page).
You'll notice there is a 'Abstract Page Data' base class for a resource. This will be used for pages where the template should be the same or very similar (for example Blog Articles). These pages will have routes assigned to them automatically based on the page titles.
When 'Routes' change we will also handle creating redirects from the old route to the new one where possible.
There is a lot to create and discuss here and most features were implemented in some way for version 1 of this bundle. Some methods by which the functionality is implemented will change and there is a lot of ways in which we are able to simplify this version of the bundle in comparison to the first version.
We hope by creating this we can provide a tool for developers and designers to easily create websites using re-usable modules which can be implemented with ease, extremely useful out-of-the-box functionality and for it to be easy to build upon.
A starting point for a front-end web application using this API will be built as well and once complete will include security features and a couple of simple examples of components which can be modified by a website administrator. Our example will be a progressive web app using VueJS and Nuxt. We have created this for our 1st version of this bundle as an experiment but it will need to be re-made. There will be a link here once there is something to see.
We encourage using as many of the packages as possible that are well maintained by large and active communities. Therefore let's start with the most up to date API Platform files and then install this bundle on top.
- Download API Platform files from GitHub as described in their 'Getting Started' instructions
- Delete the folders
/client
and/admin
- we do not need these - Remove the client and admin configurations from the
/docker-compose.yaml
file - Update the
api/Dockerfile
- Change PHP version to at least 7.4
- Remove
--with-libzip
if present - Add
COPY assets assets/
beneathCOPY src src/
- Add
exif
andxsl
to thedocker-php-ext-install
arguments (exif is to determine whether files are images and xsl is for the Inky extension working with emails using Symfony Mailer) - Add
libxslt-dev
toapk add --no-cache --virtual .build-deps
(required to install xsl) - For
LiipImagineBundle
Support- Add to
apk add --no-cache --virtual .build-deps
command the packageslibpng-dev
,libjpeg-turbo-dev
andfreetype-dev
- Add the following to include gd
docker-php-ext-configure gd --with-freetype --with-jpeg
- Add or modify to include gd
docker-php-ext-install gd
- Add to
- Start up the containers
- run
docker-compose exec php sh
to bash into the php container - run
composer require silverbackis/api-component-bundle:2.x-dev
- As described here - generate the SSH keys for JWTs. (Use the passphrase that has been generated in your .env file - in production you can generate keys using /bin/rand_string.sh which will be located in the sample project which includes the API and front-end)
mkdir -p config/jwt
openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
Be sure to run the recipe for this bundle or take a look at all the files and configurations in the repository that would normally have been executed if the recipe was run. It includes route mapping, default package configuration and a default User entity definition.
Configure your security/firewall. There is a configurable token so that requests to the API where a refresh token is returned can only be made from anotehr server. This is to protect the refresh key from being passed directly to a user. You should save the refresh key in your server-side session data and handle refreshing the JWT token that way.
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
encoders:
Silverback\ApiComponentBundle\Entity\User\AbstractUser:
algorithm: auto
providers:
user_provider:
entity:
class: Silverback\ApiComponentBundle\Entity\User\AbstractUser
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/login
stateless: true
# anonymous: true
provider: user_provider
user_checker: Silverback\ApiComponentBundle\Security\UserChecker
guard:
authenticators:
- Silverback\ApiComponentBundle\Security\TokenAuthenticator
json_login:
check_path: /login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
main:
pattern: ^/
stateless: true
anonymous: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
# https://symfony.com/doc/current/security/impersonating_user.html
switch_user: true
access_control:
- { path: ^/login, roles: ROLE_TOKEN_USER }
- { path: ^/password/reset, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [POST] }
- { path: ^/component/forms/(.*)/submit, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [POST, PATCH] }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY, methods: [POST, PUT, PATCH, DELETE] }
Each of these components should have the appropriate properties and may use Traits provided to help. E.g. a Title
on a Hero
component using a trait, or copyrightText
on the Footer
which would not use a trait
- Create
NavItem
component API resource - Create
Footer
component API resource - Create
Hero
component API resource - Create a Symfony form (with validation if you want) and
FormSuccessHandler
Adding a new component to component groups with a ComponentLocation
should be done in one action the the user of the front-end application. Otherwise this would be quite cumbersome to create the component resource in the API and then have to find the IDs of the appropriate component and component group to add a ComponentLocation
- Create a
Layout
API resource - Create 2
ComponentGroup
resources within theLayout
- Create a
Collection
component defined to display theNavItem
resource. You could also define a specific front-end component to use such asNavBar
that the web application can read to display the collection appropriately - Create a
ComponentLocation
resource placing theCollection
into the 1stComponentGroup
of theLayout
- Create
NavItem
resources for the navigation bar. You could configure yourNavItem
to have an optional relation toComponentGroup
and then insert nestedNavItem
resources that way if you desire a tree structure to your navigation - Create a
Footer
resource and aComponentLocation
to add it into the 2ndComponentGroup
for theLayout
- Create a
PageTemplate
API resource - Create 1
ComponentGroup
within thisPageTemplate
- Create a
Route
API resource which will direct a user to thePageTemplate
you've just created. You now have a route directing a user to an empty page - Create a
Hero
API resource (with a title and any other properties you defined) - add this to theComponentGroup
with aComponentLocation
resource - Create a
Form
API resource defining the Symfony form type class and handler class you'd like to use and auiComponentName
ofExampleForm
. Then add this to theComponentGroup
as above using aComponentLocation
resource.
There will be examples of this in our sample Nuxt front-end application. The application will read in all of the API resources and save them all individually in a global store. It will also listen for any changes to any resource and update it in the store. Each mixin provided that you should use in components will be reading the component's data from the global store
- Create a layout and set the 1st
ComponentGroup
to render at the top and the 2nd at the bottom. The rest of the UI layout and styling is up to you. You'll want to include where you want your page to render as per your front-end framework's instructions too. - Create a
NavBar
component which will be used to render theCollection
resource ofNavItem
resources. - Create
NavItem
component, making sure if you have a more nestedNavItem
resources within aComponentGroup
you also render these appropriately.
If you have multiple layouts, in the API you will be able to specify a UIComponentName
for the layout which will in turn use that as the layout name in the front end application which you can configure.
There will be a default template used to simply render each ComponentGroup
and ComponentLocation
resources stacked on top of each other. In the API you will be able to define a different UI component to use if you want to arrange the ComponentGroup
resources within PageTemplate
(much like we did in the Layout
)
- Create a
Hero
component which will render the title. There will be mixins so you can easily get the component's data and more in relation to whether a user is logged in or not in future. - Create a
ExampleForm
component. There will be a mixin which will help you to render all the form fields with automatic validation and so much more.
For all aspects of the front-end, this is an extremely brief overview. Examples and all functionality will be shown in far greater detail in the GitHub repository as we build it. Lots of the features were available in the 1st version of this system, but we realised now we have clarity over how it should all work and the features required, we need to start again. Watch this space!