Creating a modern web
application using Symfony API
Platform, ReactJS and Redux
by Jesus Manuel Olivas / weKnow
@jmolivas
● WHO AM I?
Jesus Manuel Olivas
jmolivas@weknowinc.com
jmolivas
jmolivas
drupal.org/u/jmolivas
jmolivas.weknowinc.com
WeGive
2,572,697
WeAre
WeKnow
Symfony
API Platform / GraphQL
 ReactJS / Redux / Saga
Ant Design
Symfony Flex
Symfony Flex … a Composer plugin for Symfony
> Symfony Flex is a Composer plugin that modifies the behavior of
the require, update, and remove composer commands.
> Symfony Flex automates the most common tasks of Symfony
applications, like installing and removing bundles and other
dependencies using recipes defined in a manifest.json file.
Directory structure
API Platform 
Framework
The API Platform Framework
REST and GraphQL
framework to build
modern API-driven
projects
https://api-platform.com/
Built on the Shoulders of Giants
> Extend the framework with thousands of existing Symfony
bundles and React components.
> The API component includes the Symfony 4 flex, the Doctrine ORM.
Client-side components and Admin based on React and a Docker
configuration ready to startup your project using one single command.
> 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 Schema Admin CRUD
Try API-Platform
# Clone code repository

git clone https://github.com/api-platform/api-platform.git
Recommendations and adjustments
> Update route prefix at api/config/routes/api_platform.yaml
file.
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
ahoy up
# Open browser
open http://localhost/
Add more formats
Update api/config/packages/api_platform.yaml adding:
formats:
jsonld: [‘application/ld+json'] # first one is the default format
json: ['application/json']
jsonhal: ['application/hal+json']
xml: ['application/xml', 'text/xml']
yaml: ['application/x-yaml']
csv: ['text/csv']
html: ['text/html']
Add Dependencies
> Add new entities to api/src/Entity/ directory:

ahoy console make:entity



> Remove default entity

api/src/Entity/Greeting.php
api/src/Entity/Post.php 1/3
<?php

namespace AppEntity;



use ApiPlatformCoreAnnotationApiResource;

use DoctrineORMMapping as ORM;

use SymfonyComponentValidatorConstraints as Assert;
/**

* @ApiResource

* @ORMTable(name="post")

* @ORMEntity

*/
class Post
{
…
}
api/src/Entity/Post.php 2/3
/**

* @ORMId

* @ORMGeneratedValue(strategy="AUTO")

* @ORMColumn(type="integer")

*/

private $id;
/**

* @ORMColumn

* @AssertNotBlank

*/

public $title = '';

/**

* @ORMColumn
* @AssertNotBlank
*/
public $body = '';
api/src/Entity/Post.php 3/3
/**
* @ORMManyToOne(targetEntity="PostType")
* @ORMJoinColumn(name="post_type_id", referencedColumnName="id", nullable=false)
*/
public $type;
public function getId(): int
{
return $this->id;
}
Tracking Database changes
# Add dependency
ahoy composer require migrations
# Execute command(s)
ahoy console make:migration
ahoy console doctrine:migrations:migrate
Add FOSUserBundle
# Add dependency
composer require friendsofsymfony/user-bundle
composer require symfony/swiftmailer-bundle
https://symfony.com/doc/current/bundles/FOSUserBundle/index.html
https://jolicode.com/blog/do-not-use-fosuserbundle
Initialize the project
> Drop and Create Database
> Execute Migrations
> Populate Entities with Data
bin/console init
Use:

hautelook/alice-bundle 

doctrine/data-fixtures
Loading Posts using the Browser
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
curl -X GET "http://localhost:8080/api/posts" 
-H "accept: application/json"
curl -X GET "http://localhost:8080/api/posts/1" 
-H "accept: application/ld+json"
ADD Posts from CLI
curl -X POST "http://localhost:8080/api/posts" 
-H "accept: application/ld+json" 
-H "Content-Type: application/ld+json" 
-d '{ "title": "Post create from CLI", "body": "body-
less", "type": "/api/post_types/1"}'
UPDATE and REMOVE Posts from CLI
curl -X PUT "http://localhost:8080/api/posts/9" 
-H "accept: application/ld+json" 
-H "Content-Type: application/ld+json" 
-d '{ "title": "Updated from CLI"}'
curl -X DELETE "http://localhost:8080/api/posts/10" 
-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
# api/src/Entity/Post.php & PostType.php
* @ApiResource(attributes={
* "normalization_context"={"groups"={"read"}},
* "denormalization_context"={"groups"={"write"}}
* })
Serialization Relations (Post => PostType) 2/2
# Add use keyword to class
use SymfonyComponentSerializerAnnotationGroups;
# Add use keyword to properties
* @Groups({"read"})
* @Groups({"read", "write"})
GraphQL
GraphQL
GraphQL offers significantly more flexibility for integrators.
Allows you to define in detail the only the data you want.
GraphQL lets you replace multiple REST requests with a single
call to fetch the data you specify.
Add GraphQL
To enable GraphQL and GraphiQL interface in your API, simply require
the graphql-php package using Composer:
composer require webonyx/graphql-php
open http://localhost:8080/api/graphql
Disable GraphiQL
Update api/config/packages/api_platform.yaml adding:
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" 
-d '{ "query": "{ post(id:"/api/posts/1") { id,
title, body }}" }' 
http://localhost:8080/api/graphql
Load resource relations using GraphQL
{
post (id:"/api/posts/1") {
title,
body,
type {
id,
name,
}
}
}
Load resource relations using GraphQL form the CLI
curl -X POST 
-H "Content-Type: application/json" 
-d '{ "query": "{ post(id:"/api/posts/1") { id, title,
body, type { id, name } }}" }' 
http://localhost:8080/api/graphql
JWT
JWT Dependencies
# JWT
composer require lexik/jwt-authentication-bundle
JWT Refresh

gesdinet/jwt-refresh-token-bundle
JWT Events (create)
# config/services.yaml

AppEventListenerJWTCreatedListener:

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

AppEventListenerAuthenticationSuccessListener:

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
> Use webpack instead of roadhog.
> Use apollo-fetch for GraphQL calls.
> Use jwt-decode to interact with JWT.
> Use LocalStorage for simple key/values storage.
> Use IndexedDB for encrypted and/or more complex data structures.
> Use Socket-IO + Redis to sync API with ReactJS client.
Tips
Directory Structure
├── package.json
├── src
│   ├── components
│   ├── constants.js
│   ├── index.js
│   ├── models
│   ├── router.js
│   ├── routes
│   └── services
└── webpack.config.js
constants.js
export const API_URL = process.env.API_ENTRY_POINT;
export const GRAPHQL_URL = `${API_URL}/api/graphql`;
export const NOTIFICATION_URL = process.env.NOTIFICATION_URL;
export const NOTIFICATION_ICON = {
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';
const app = dva({

…

});



# Register global models

app.model(auth);

app.model(local);

app.model(ui);

app.router(require('./router'));

app.start(‘#root');
router.js 1/2
import …

import AuthRoute from './components/Auth/AuthRoute';
export default function RouterConfig({ history, app }) {

const Login = dynamic({

app,

component: () => import('./routes/Login'),

});
const routes = [{

path: '/posts',

models: () => [

import('./models/posts')

],

component: () => import('./routes/Posts'),

}, {
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()}
then={(isValid) => isValid ?
<Route path={this.props.path} component={this.props.component} />
:
<Redirect to={{ pathname: '/login' }}/>
}
/>
);
};
src/models/posts.js 1/3
import {Route, Redirect} from "dva/router";

import * as postService from '../services/base';

import _map from 'lodash/map';

…
export default {

namespace: ‘posts',

state: {

list: [],

total: 0

},

reducers: {

save(state, { payload: { list, total } }) {

return { ...state, list, total};

},
},
src/models/posts.js 2/3
effects: {

*fetch({ payload: { … }, { call, put }) {

let { list, total } = yield select(state => state.posts);

const graphQuery = `{

posts(first: 10) {

edges {

node {

id, _id, title, body,

type {

id, name, machineName

}

}

}

}

}`;
src/models/posts.js 2/3
#  Async call with Promise support.
const posts = yield call(postService.fetch, 'posts', { … });
const list = _map(posts.data.posts.edges, posts => posts.node)
# Action triggering.
yield put({
type: 'save',
payload: {
list,
total,
},
});
Directory Structure
src/
├── components
   ├── Auth
   ├── Generic
   ├── Layout
   └── Posts
      ├── ProjectBase.js
      ├── ProjectEdit.js
      ├── ProjectList.js
      └── ProjectNew.js
Thank you … Questions?

Creating a modern web application using Symfony API Platform, ReactJS and Redux DrupalCampLA

  • 1.
    Creating a modernweb application using Symfony API Platform, ReactJS and Redux by Jesus Manuel Olivas / weKnow @jmolivas
  • 2.
    ● WHO AMI? Jesus Manuel Olivas jmolivas@weknowinc.com jmolivas jmolivas drupal.org/u/jmolivas jmolivas.weknowinc.com
  • 3.
  • 4.
  • 5.
  • 6.
    Symfony API Platform /GraphQL  ReactJS / Redux / Saga Ant Design
  • 7.
  • 8.
    Symfony Flex …a Composer plugin for Symfony > Symfony Flex is a Composer plugin that modifies the behavior of the require, update, and remove composer commands. > Symfony Flex automates the most common tasks of Symfony applications, like installing and removing bundles and other dependencies using recipes defined in a manifest.json file.
  • 9.
  • 10.
  • 11.
    The API Platform Framework RESTand GraphQL framework to build modern API-driven projects https://api-platform.com/
  • 12.
    Built on theShoulders of Giants > Extend the framework with thousands of existing Symfony bundles and React components. > The API component includes the Symfony 4 flex, the Doctrine ORM. Client-side components and Admin based on React and a Docker configuration ready to startup your project using one single command. > Reuse all your Symfony, React and Docker skills and benefit of their high quality docs; you are in known territory.
  • 13.
    The API PlatformComponents API Schema Admin CRUD
  • 14.
    Try API-Platform # Clonecode repository
 git clone https://github.com/api-platform/api-platform.git
  • 15.
    Recommendations and adjustments >Update route prefix at api/config/routes/api_platform.yaml file. api_platform:
 …
 prefix: /api > Update admin/.env and client/.env files (change protocol and port). REACT_APP_API_ENTRYPOINT=http://localhost:8080/api
  • 16.
    Start containers …and grab water, coffee, or a beer. # Start containers ahoy up # Open browser open http://localhost/
  • 17.
    Add more formats Updateapi/config/packages/api_platform.yaml adding: formats: jsonld: [‘application/ld+json'] # first one is the default format json: ['application/json'] jsonhal: ['application/hal+json'] xml: ['application/xml', 'text/xml'] yaml: ['application/x-yaml'] csv: ['text/csv'] html: ['text/html']
  • 18.
    Add Dependencies > Addnew entities to api/src/Entity/ directory:
 ahoy console make:entity
 
 > Remove default entity
 api/src/Entity/Greeting.php
  • 23.
    api/src/Entity/Post.php 1/3 <?php
 namespace AppEntity;
 
 useApiPlatformCoreAnnotationApiResource;
 use DoctrineORMMapping as ORM;
 use SymfonyComponentValidatorConstraints as Assert; /**
 * @ApiResource
 * @ORMTable(name="post")
 * @ORMEntity
 */ class Post { … }
  • 24.
    api/src/Entity/Post.php 2/3 /**
 * @ORMId
 *@ORMGeneratedValue(strategy="AUTO")
 * @ORMColumn(type="integer")
 */
 private $id; /**
 * @ORMColumn
 * @AssertNotBlank
 */
 public $title = '';
 /**
 * @ORMColumn * @AssertNotBlank */ public $body = '';
  • 25.
    api/src/Entity/Post.php 3/3 /** * @ORMManyToOne(targetEntity="PostType") *@ORMJoinColumn(name="post_type_id", referencedColumnName="id", nullable=false) */ public $type; public function getId(): int { return $this->id; }
  • 26.
    Tracking Database changes #Add dependency ahoy composer require migrations # Execute command(s) ahoy console make:migration ahoy console doctrine:migrations:migrate
  • 27.
    Add FOSUserBundle # Adddependency composer require friendsofsymfony/user-bundle composer require symfony/swiftmailer-bundle https://symfony.com/doc/current/bundles/FOSUserBundle/index.html https://jolicode.com/blog/do-not-use-fosuserbundle
  • 28.
    Initialize the project >Drop and Create Database > Execute Migrations > Populate Entities with Data bin/console init Use:
 hautelook/alice-bundle 
 doctrine/data-fixtures
  • 29.
    Loading Posts usingthe Browser 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
  • 30.
    Loading Posts usingthe CLI curl -X GET "http://localhost:8080/api/posts" -H "accept: application/json" curl -X GET "http://localhost:8080/api/posts/1" -H "accept: application/ld+json"
  • 31.
    ADD Posts fromCLI curl -X POST "http://localhost:8080/api/posts" -H "accept: application/ld+json" -H "Content-Type: application/ld+json" -d '{ "title": "Post create from CLI", "body": "body- less", "type": "/api/post_types/1"}'
  • 32.
    UPDATE and REMOVEPosts from CLI curl -X PUT "http://localhost:8080/api/posts/9" -H "accept: application/ld+json" -H "Content-Type: application/ld+json" -d '{ "title": "Updated from CLI"}' curl -X DELETE "http://localhost:8080/api/posts/10" -H "accept: application/json"
  • 33.
    Serialization > API Platformallows 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.
  • 34.
    Serialization Relations (Post=> PostType) 1/2 # api/src/Entity/Post.php & PostType.php * @ApiResource(attributes={ * "normalization_context"={"groups"={"read"}}, * "denormalization_context"={"groups"={"write"}} * })
  • 35.
    Serialization Relations (Post=> PostType) 2/2 # Add use keyword to class use SymfonyComponentSerializerAnnotationGroups; # Add use keyword to properties * @Groups({"read"}) * @Groups({"read", "write"})
  • 36.
  • 37.
    GraphQL GraphQL offers significantly moreflexibility for integrators. Allows you to define in detail the only the data you want. GraphQL lets you replace multiple REST requests with a single call to fetch the data you specify.
  • 38.
    Add GraphQL To enableGraphQL and GraphiQL interface in your API, simply require the graphql-php package using Composer: composer require webonyx/graphql-php open http://localhost:8080/api/graphql
  • 39.
    Disable GraphiQL Update api/config/packages/api_platform.yamladding: api_platform: # ... graphql: graphiql: enabled: false # ...
  • 40.
    Load resource usingGraphQL { post (id:"/api/posts/1") { id, title, body } }
  • 41.
    Load resource usingGraphQL form the CLI curl -X POST -H "Content-Type: application/json" -d '{ "query": "{ post(id:"/api/posts/1") { id, title, body }}" }' http://localhost:8080/api/graphql
  • 42.
    Load resource relationsusing GraphQL { post (id:"/api/posts/1") { title, body, type { id, name, } } }
  • 43.
    Load resource relationsusing GraphQL form the CLI curl -X POST -H "Content-Type: application/json" -d '{ "query": "{ post(id:"/api/posts/1") { id, title, body, type { id, name } }}" }' http://localhost:8080/api/graphql
  • 44.
  • 45.
    JWT Dependencies # JWT composerrequire lexik/jwt-authentication-bundle JWT Refresh
 gesdinet/jwt-refresh-token-bundle
  • 46.
    JWT Events (create) #config/services.yaml
 AppEventListenerJWTCreatedListener:
 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);
 }
  • 47.
    JWT Events (success) #config/services.yaml
 AppEventListenerAuthenticationSuccessListener:
 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);
 }
  • 48.
  • 49.
    dvajs/dva - Reactand redux based framework.  https://github.com/dvajs/dva
  • 50.
    React / Redux/ Saga / AntDesig
  • 51.
    > Use webpackinstead of roadhog. > Use apollo-fetch for GraphQL calls. > Use jwt-decode to interact with JWT. > Use LocalStorage for simple key/values storage. > Use IndexedDB for encrypted and/or more complex data structures. > Use Socket-IO + Redis to sync API with ReactJS client. Tips
  • 52.
    Directory Structure ├── package.json ├──src │   ├── components │   ├── constants.js │   ├── index.js │   ├── models │   ├── router.js │   ├── routes │   └── services └── webpack.config.js
  • 53.
    constants.js export const API_URL= process.env.API_ENTRY_POINT; export const GRAPHQL_URL = `${API_URL}/api/graphql`; export const NOTIFICATION_URL = process.env.NOTIFICATION_URL; export const NOTIFICATION_ICON = { info: 'info', warning: 'exclamation', error: 'close', success: 'check' };
  • 54.
    index.js import dva from'dva';
 import auth from './models/auth';
 import local from './models/local';
 import ui from './models/ui'; const app = dva({
 …
 });
 
 # Register global models
 app.model(auth);
 app.model(local);
 app.model(ui);
 app.router(require('./router'));
 app.start(‘#root');
  • 55.
    router.js 1/2 import …
 importAuthRoute from './components/Auth/AuthRoute'; export default function RouterConfig({ history, app }) {
 const Login = dynamic({
 app,
 component: () => import('./routes/Login'),
 }); const routes = [{
 path: '/posts',
 models: () => [
 import('./models/posts')
 ],
 component: () => import('./routes/Posts'),
 }, { path: '/posts/:id',
 models: () => [
 import('./models/projects'),
 ],
 component: () => import('./routes/Posts'),
 }];
  • 56.
    router.js 2/2 return ( <Routerhistory={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>
 );
 }
  • 57.
    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()} then={(isValid) => isValid ? <Route path={this.props.path} component={this.props.component} /> : <Redirect to={{ pathname: '/login' }}/> } /> ); };
  • 58.
    src/models/posts.js 1/3 import {Route,Redirect} from "dva/router";
 import * as postService from '../services/base';
 import _map from 'lodash/map';
 … export default {
 namespace: ‘posts',
 state: {
 list: [],
 total: 0
 },
 reducers: {
 save(state, { payload: { list, total } }) {
 return { ...state, list, total};
 }, },
  • 59.
    src/models/posts.js 2/3 effects: {
 *fetch({payload: { … }, { call, put }) {
 let { list, total } = yield select(state => state.posts);
 const graphQuery = `{
 posts(first: 10) {
 edges {
 node {
 id, _id, title, body,
 type {
 id, name, machineName
 }
 }
 }
 }
 }`;
  • 60.
    src/models/posts.js 2/3 #  Asynccall with Promise support. const posts = yield call(postService.fetch, 'posts', { … }); const list = _map(posts.data.posts.edges, posts => posts.node) # Action triggering. yield put({ type: 'save', payload: { list, total, }, });
  • 61.
    Directory Structure src/ ├── components   ├── Auth    ├── Generic    ├── Layout    └── Posts       ├── ProjectBase.js       ├── ProjectEdit.js       ├── ProjectList.js       └── ProjectNew.js
  • 62.
    Thank you …Questions?