Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
250 views

Creating A Modern Web App Using Symfony Api Platform

This document provides instructions for creating a modern web application using Symfony API Platform, ReactJS and Redux. It discusses setting up the Symfony API with API Platform and enabling GraphQL support. It also covers configuring JWT authentication and adding React/Redux with AntDesign for the frontend. The document includes code snippets for entity definitions, serialization, GraphQL queries and JWT events. It provides recommendations for directory structure and libraries when using React/Redux with this stack.

Uploaded by

lawson
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
250 views

Creating A Modern Web App Using Symfony Api Platform

This document provides instructions for creating a modern web application using Symfony API Platform, ReactJS and Redux. It discusses setting up the Symfony API with API Platform and enabling GraphQL support. It also covers configuring JWT authentication and adding React/Redux with AntDesign for the frontend. The document includes code snippets for entity definitions, serialization, GraphQL queries and JWT events. It provides recommendations for directory structure and libraries when using React/Redux with this stack.

Uploaded by

lawson
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 59

Creating a modern web

application using Symfony API


Platform, ReactJS and Redux
by Jesus Manuel Olivas & Eduardo Garcia
@jmolivas | @enzolutions
Who We Are?

Jesus Manuel Olivas


jmolivas@weknowinc.com
jmolivas
jmolivas
http://drupal.org/u/jmolivas
http://jmolivas.weknowinc.com
Who We Are?

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

> 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

docker-compose up -d

# 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 Blog and Tag entities, remove default Greeting entity

> Add new entities to api/src/Entity/ directory:



api/src/Entity/Post.php

api/src/Entity/PostType.php

api/src/Entity/User.php


> 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

*/

public $body = '';


api/src/Entity/Post.php 3/3
/**

* @ORM\ManyToOne(targetEntity="PostType")

* @ORM\JoinColumn(name="post_type_id", referencedColumnName="id", nullable=false)

*/

public $type;

public function getId(): int

return $this->id;

}
Tracking Database changes

# Add dependency

composer require doctrine/doctrine-migrations-bundle

# Execute command(s)

doctrine:migrations:diff

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

NOTE: Use hautelook/AliceBundle or willdurand/BazingaFakerBundle


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 Symfony\Component\Serializer\Annotation\Groups;

# 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,

machineName

}
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, machineName } }}" }' \

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

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 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.


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
Any questions?

You might also like