Docker

Comandos útiles:

// ingresar al contenedor del servidor
$ docker-compose exec NOMBRE_CONTENEDOR_SERVIDOR bash
//Ingresar al contenedor del servidor de BD
$ docker-compose exec NOMBRE_CONTENEDOR_DB mysql -u USUARIO --password=CONTRASEÑA

API PLATFORM

Conceptos

Serialization: la información es transformada de un objeto a un formato distinto ej: JSON

Normalization: la información es transformada de un objeto a un array. Este es un paso intermedio entre la serialización a la deserializacion. Ej: objeto -> (normalizado) Array -> Json.
En API PLATFORM, la normalization (normalizationContext) es cuando se lee información y la denormalization (denormalizationContext) cuando se escribe información

Deserilaization: la información es transformada de un formato distinto ej: JSON a un objeto

Instalacion en Symfony:

$ composer require api
$ composer require jwt-auth
$ mkdir var/jwt
$ echo "thepass" | openssl genpkey -out var/jwt/private.pem -pass stdin -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
$  echo "thepass" | openssl pkey -in var/jwt/private.pem -passin stdin -out var/jwt/public.pem -pubout
$ setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX var/jwt
$ setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX var/jwt

JWT: https://api-platform.com/docs/core/jwt/#jwt-authentication

Test

$ composer require --dev test-pack http-client justinrainbow/json-schema
// modificar phpunit.xml.dist con la versión de PHP unit que necesitamos
$ php bin/phpunit
// En env.test agregar la base de datos que vamos a usar para test
// DATABASE_URL=mysql://root:thepass@127.0.0.1:3306/database_test?serverVersion=5.7
$ php bin/console doctrine:create:database --env=dev
$ composer require --dev alice
$ composer require logger

Configuraciones de un recurso:


/**
 * @ApiResource(
 *     collectionOperations={"get", "post"}, -> add/remove operaciones de coleccion
 *     itemOperations={
 *         "get" = {"path"= "/description_de_producto/{id}"}, -> cambia la url para ese endpoint 
 *         "put", 
 *         "delete"} 
 *     ) 
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Productos
{


/**
 * @ApiResource(
 *     collectionOperations={"get", "post"}, -> add/remove operaciones de coleccion
 *     itemOperations={"get", "put", "delete"} -> add/remove operaciones de item
 *     shortName="Product" -> cambia el nombre de la url que por defecto toma el de la entidad
 *     ) 
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Productos
{


Normalizer / DeNormalizer

/**
 * @ApiResource(
 *     collectionOperations={"get", "post"},
 *     itemOperations={"get", "put", "delete"},
 *     normalizationContext={ -> cuando se lee información de la API
 *         "groups"={"products:read"}, "swagger_definition_name"="Read",
 *     },
 *     denormalizationContext={ -> cuando se escribe información de la API
 *         "groups"={"products:write"}, "swagger_definition_name"="Write",
 *     },
 *     ) 
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Productos
{

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"products:read", "products:write"})
     */
    private $title;

Serialization name

class Productos
{
    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"products:read", "products:write"})
     * @SerializedName("description")
     */
     public function setTextDescription() {

Filters

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
/**
 * @ApiResource(
 *     collectionOperations={"get", "post"},
 *     itemOperations={"get", "put", "delete"},
 *     attributes={
 *         "pagination_items_per_page"=10 -> paginación
 *         "formats"={"jsonld", "json", "html", "jsonhal", "csv"={"text/csv"}} -> formatos que acepta la API, hay q activarlos en el config de la API
 *     }
 *     ) 
 * @ApiFilter(BooleanFilter::class, properties={"isPublished"})
 * @ApiFilter(SearchFilter::class, properties={"title": "partial"}) -> partial, exact, start, end, word_start
 * @ApiFilter(RangeFilter::class, properties={"price"})
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 * @ApiFilter(PropertyFilter::class) -> permite devolver solo los campos que queremos: ?properties[]=title&properties[]=shortDescription
 */

class Productos
{


Seguridad en end points

/**
 * @ApiResource(
 *     collectionOperations={
 *          "get",
 *          "post"={"security"="is_granted('ROLE_USER')"}
 *          "put"={
 *              "security"="is_granted('ROLE_USER') and object.getUser() == user",
 *              "security"="Only the creator can edit a cheese listing"
 *              },
 *     },
 *     ) 
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Productos
{


Generar rutas en base a IRIs:

public function login(IriConverterInterface $iriConverter)
    {
        return new Response(null, 204, [
            'Location' => $iriConverter->getIriFromItem($this->getUser())
        ]);
    }

Deshabilitar docs en producción:

$ touch config/packages/prod/api_platform.yml
//Dentro de este archivo agregar
api_platform:
    enable_docs: false
    enable_entrypoint: false

React JS

Instalar React

$ yarn add eslint --dev
$ yarn add react react-dom --dev
$ yarn add @babel/preset-react@^7.0.0 --dev
$ yarn add eslint-plugin-react --dev
$ yarn add prop-types --dev 
$ yarn add babel-plugin-transform-react-remove-prop-types --dev 
$ yarn add sass-loader@^11.0.0 sass --dev 
Agrega este contenido dentro de .eslintrc.js:  

module.exports = { extends: ['eslint:recommended', 'eslint-plugin-react:react/recommended'], parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { jsx: true } }, env: { browser: true, es6: true, node: true }, rules: { "no-console": 0, "no-unused-vars": 0 } };

Crear App

$ npx create-react-app myapp
$ cd myapp
$ yarn start

// https://saurabhshah23.medium.com/react-js-architecture-features-folder-structure-design-pattern-70b7b9103f22

React & Symfony

En webpack.config.json agregar:
//enable React
.enableReactPreset()

// eliminar proptypes en produccion
.configureBabel((babelConfig) => {
    if (Encore.isProduction()) {
        babelConfig.plugins.push(
            'transform-react-remove-prop-types'
        );
    }
})

React Components

export defualt class AComponent extends React.Component {

    construct(props) {
        super(props);
        this.state = {
            row = null   
        }
    }

    //Este método es obligatorio
    render() {
    }
}

Functional Components

const MyButton = ({ disabled, text }) => (
    
);
MyButton.defaultProps = {
    text: 'My Button',
    disabled: false
};
export default MyButton;

Props y state

Props son valores que recibe el componente que son inmutables, el componente no las va a cambiar
cuando se realiza un evento.
State si las puede cambiar el componente

//Cuando se va a cambiar el state pero se va a utilizar un valor del mismo state se tiene que pasar con una callback
//donde state es el estado actual
this.setState(state => ({
    ...state,
    fourth: state.doneMessage
})

Además hay que crear nuevos objetos cuando queremos modificar un objeto del state, para mantener la inmutabilidad

//...
this.state = {
    data : [
        {//..},
        {//..},
    ]
}

Si queremos agrega un elemento a data hay que hacerlo de esta manera

someFunction() {
    const newData = [...this.state.data, {/...}]
}
Por que si lo hacemos así

const newData = this.state.data;
newData.push({//..})
Estamos modificando el objeto original.


Modificar un objeto dentro de un array manteniendo la inmutabilidad

someFunction(id) {
    this.setState((prevState) => {
        return {
            data: prevState.data.map(d => { -> map nos devuelve una copia (explicacion anterior)
                if (d.id !== id) {
                    return d;
                }
                //Pero dentro de esa copia los objetos son los originales entonces con Object assing devuelve un nuevo objeto donde el 3er objeto es combinado al 2do y el 2do al 1ero
                return Object.assign({}, d, {someProp: true});
            })
      };
});

lifecycles

getDerivedStateFromProps() : This method allows you to update the state of
the component based on property values of the component. This method is called
when the component is initially rendered and when it receives new property
values.
render() : Returns the content to be rendered by the component. This is called
when the component is first mounted to the DOM, when it receives new
property values, and when setState() is called.
componentDidMount() : This is called after the component is mounted to the
DOM. This is where you can perform component initialization work, such as
fetching data.
shouldComponentUpdate() : You can use this method to compare new state or
props with current state or props. Then, you can return false if there's no need to
re-render the component. This method is used to to make your components more
efficient.
getSnapshotBeforeUpdate() : This method lets you perform operations
directly on DOM elements of your component before they're actually committed
to the DOM. The difference between this method and render() is that
getSnapshotBeforeUpdate() isn't asynchronous. With render() , there's a
good chance that the DOM structure could change between when it's called and
when the changes are actually made in the DOM.
componentDidUpdate() : This is called when the component is updated. It's rare
that you'll have to use this method.

Portals

https://reactjs.org/docs/portals.html
A component that should be rendered as a dialog probably
doesn't need to be mounted at the parent. Using a portal, you can control specifically where
your component's content is rendered

Paquetes utiles

$ npm install --save redux-saga
$ npm install --save redux
$ npm i -S react-redux
$ npm i -D redux-devtools