Creating A Modern Web App Using Symfony Api Platform
Creating A Modern Web App Using Symfony Api Platform
Eduardo Garcia
enzo@weknowinc.com
enzolutions
enzolutions
http://drupal.org/u/enzo
http://enzolutions.com
WeGive
WeAre
WeKnow
Symfony
API Platform / GraphQL
ReactJS / Redux / Saga
Ant Design
Symfony Flex
Symfony Flex … a Composer plugin for Symfony
> Reuse all your Symfony, React and Docker skills and benefit of their high
quality docs; you are in known territory.
The API Platform Components
api_platform:
…
prefix: /api
> Update admin/.env and client/.env files (change protocol and port).
REACT_APP_API_ENTRYPOINT=http://localhost:8080/api
Start containers … and grab water, coffee, or a beer.
# Start containers
docker-compose up -d
# Open browser
open http://localhost/
Add more formats
Update api/config/packages/api_platform.yaml adding:
formats:
json: ['application/json']
jsonhal: ['application/hal+json']
yaml: ['application/x-yaml']
csv: ['text/csv']
html: ['text/html']
Add Blog and Tag entities, remove default Greeting entity
> Remove default entity
api/src/Entity/Greeting.php
api/src/Entity/Post.php 1/3
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource
* @ORM\Table(name="post")
* @ORM\Entity
*/
class Post
}
api/src/Entity/Post.php 2/3
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column
* @Assert\NotBlank
*/
public $title = '';
/**
* @ORM\Column
* @Assert\NotBlank
*/
* @ORM\ManyToOne(targetEntity="PostType")
*/
public $type;
return $this->id;
}
Tracking Database changes
# Add dependency
# Execute command(s)
doctrine:migrations:diff
doctrine:migrations:migrate
Add FOSUserBundle
# Add dependency
https://symfony.com/doc/current/bundles/FOSUserBundle/index.html
https://jolicode.com/blog/do-not-use-fosuserbundle
Initialize the project
> Drop and Create Database
bin/console init
http://localhost:8080/api/posts
http://localhost:8080/api/posts.json
http://localhost:8080/api/posts.jsonld
http://localhost:8080/api/posts/1
http://localhost:8080/api/posts/1.json
Loading Posts using the CLI
-H "accept: application/json"
-H "accept: application/ld+json"
ADD Posts from CLI
-H "accept: application/ld+json" \
-H "Content-Type: application/ld+json" \
-H "accept: application/ld+json" \
-H "Content-Type: application/ld+json" \
-H "accept: application/json"
Serialization
> API Platform allows to specify the which attributes of the resource are
exposed during the normalization (read) and denormalization (write)
process. It relies on the serialization (and deserialization) groups feature of
the Symfony Serializer component.
> In addition to groups, you can use any option supported by the Symfony
Serializer such as enable_max_depth to limit the serialization depth.
Serialization Relations (Post => PostType) 1/2
* @ApiResource(attributes={
* "normalization_context"={"groups"={"read"}},
* "denormalization_context"={"groups"={"write"}}
* })
Serialization Relations (Post => PostType) 2/2
use Symfony\Component\Serializer\Annotation\Groups;
* @Groups({"read"})
* @Groups({"read", "write"})
GraphQL
GraphQL
open http://localhost:8080/api/graphql
Disable GraphiQL
api_platform:
# ...
graphql:
graphiql:
enabled: false
# ...
Load resource using GraphQL
post (id:"/api/posts/1") {
id,
title,
body
}
Load resource using GraphQL form the CLI
curl -X POST \
-H "Content-Type: application/json" \
http://localhost:8080/api/graphql
Load resource relations using GraphQL
{
post (id:"/api/posts/1") {
title,
body,
type {
id,
name,
machineName
}
Load resource relations using GraphQL form the CLI
curl -X POST \
-H "Content-Type: application/json" \
http://localhost:8080/api/graphql
JWT
JWT Dependencies
# JWT
JWT Refresh
gesdinet/jwt-refresh-token-bundle
JWT Events (create)
# config/services.yaml
App\EventListener\JWTCreatedListener:
tags:
- {
name: kernel.event_listener,
event: lexik_jwt_authentication.on_jwt_created,
method: onJWTCreated
}
# src/EventListener/JWTCreatedListener.php
public function onJWTCreated(JWTCreatedEvent $event)
{
$data = $event->getData();
$user = $event->getUser();
$data['organization'] = $user->getOrganization()->getId();
$event->setData($data);
}
JWT Events (success)
# config/services.yaml
App\EventListener\AuthenticationSuccessListener:
tags:
- {
name: kernel.event_listener,
event: lexik_jwt_authentication.on_authentication_success,
method: onAuthenticationSuccessResponse
}
# src/EventListener/AuthenticationSuccessListener.php
public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event)
{
$data = $event->getData();
$user = $event->getUser();
$data[‘roles'] = $user->getOrganization()->getRoles();
$event->setData($data);
}
React+Redux+Saga+
AntDesign
dvajs/dva - React and redux based framework.
https://github.com/dvajs/dva
React / Redux / Saga / AntDesig
Tips
> Use IndexedDB for encrypted and/or more complex data structures.
└── webpack.config.js
constants.js
export const API_URL = process.env.API_ENTRY_POINT;
info: 'info',
warning: 'exclamation',
error: 'close',
success: 'check'
};
index.js
import dva from 'dva';
import auth from './models/auth';
import local from './models/local';
import ui from './models/ui';
app.router(require('./router'));
app.start(‘#root');
router.js 1/2
import …
import AuthRoute from './components/Auth/AuthRoute';
path: '/posts/:id',
models: () => [
import('./models/projects'),
],
component: () => import('./routes/Posts'),
}];
router.js 2/2
return (
<Router history={history}>
<Switch>
<Route exact path="/" render={() => (<Redirect to="/login" />)} />
<Route exact path="/login" component={Login} />
{
routes.map(({ path, onEnter, ...dynamics }, key) => (
<AuthRoute
key={key} exact path={path}
component={dynamic({
app,
...dynamics,
})}
/>
))
}
</Switch>
</Router>
);
}
src/components/Auth/AuthRoute.js
import {Route, Redirect} from "dva/router";
…
class AuthRoute extends Route {
async isTokenValid() {
# Check for token and refresh if not valid
}
render() {
return (
<Async
promise={this.isTokenValid()}
total,
},
});
Directory Structure
src/
├── components
├── Auth
├── Generic
├── Layout
└── Posts
├── ProjectBase.js
├── ProjectEdit.js
├── ProjectList.js
└── ProjectNew.js
Any questions?