Realtime w PHP z użyciem Firestore.

git clone https://github.com/cocoders/phpers-summit-2019.git

drive: https://bit.ly/2kvkR2p

slajdy: https://slides.com/leszekprabucki/phpers-summit-2019

Po co nam przetwarzanie w czasie rzeczywistym?

PHP - ograniczona możliwość 

Jakie możliwości mamy w PHPie odnośnie przetwarzania?

Firestore, Ratchet, Mercure, Pusher, Websocekts, ...

Nasz wybór Firebase (Firestore) - bardzo łatwo zintegrować z PHP i Angularem

Czym jest firebase/firestore?

Nasz projekt

#language: pl
Funkcja:
  Jako klient chciałbym móc zobaczyć aktualne menu piwa
  Żeby nie chodzić bez sensu do baru

  Scenariusz: Nowe piwo w menu
    Gdy barman doda nowe piwa:
    | nazwa       | opis                                         |                                                                                                                                                                                                                                                                                                                                                                  
    | California  | California, czyli AIPA w naszym wykonaniu... |
    Wtedy klient powinien zobaczyć 1 piwo
#language: pl
Funkcja:
  Jako klient chciałbym móc zobaczyć aktualne menu piwa
  Żeby nie chodzić bez sensu do baru

  Scenariusz: Piwo dostępne w menu się skończyło
    Zakładając że barman dodał piwa:
    | nazwa        | opis                                                                                                                                                                                                                                                                                                                                                                              |
    | California   | California, czyli AIPA w naszym wykonaniu.                            |
    | Harry        | Harry wyjechał na studia do USA. Jako anglik pijał herbatę earl grey. |
    | Nonsens Pils | Klasyczny pils z małym twistem w postaci herbaty earl grey.           |                                                                                                                                                                                 |
    Kiedy barman stwierdzi, że piwo "California" się skończyło
    Wtedy klient powinien zobaczyć 2 piwa

Implementacja PHP

Nowy projekt firebase

Nowy projekt firebase

Nowy projekt firebase

Nowy projekt firebase

Nowy projekt firebase

Klucze

RPC framework - remote procedure call

Stworzony przez googla

gRPC

https://grpc.io

Zaczynamy od Dockera który ma gRPC na pokładzie

docker-compose.yml

version: '3.7'

services:
  php:
    image: cocoders/phpers-summit-2019-symfony
    environment:
      - GOOGLE_APPLICATION_CREDENTIALS=/var/www/beer/key/key.json
    networks:
      - default
      - nginx-proxy
    volumes:
      - .:/var/www/beer
    expose:
      - 8000
    environment:
      - VIRTUAL_HOST=beer.local
      - VIRTUAL_PORT=8000

  angular:
    image: cocoders/phpers-summit-2019-angular
    volumes:
      - .:/var/www/beer
    networks:
      - default
      - nginx-proxy
    expose:
      - 4200
    environment:
      - VIRTUAL_HOST=frontend.beer.local
      - VIRTUAL_PORT=4200
  
  nginx-proxy:
    image: jwilder/nginx-proxy:alpine
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - default
      - nginx-proxy

  whoami:
    image: jwilder/whoami
    networks:
      - default
      - nginx-proxy

networks:
  nginx-proxy:
    external:
      name: nginx-proxy
version: '3.7'

services:
  php:
    image: cocoders/phpers-summit-2019-symfony
    environment:
      - GOOGLE_APPLICATION_CREDENTIALS=/var/www/beer/key/key.json
    networks:
      - default
      - nginx-proxy
    volumes:
      - .:/var/www/beer
    expose:
      - 8000
    environment:
      - VIRTUAL_HOST=beer.local
      - VIRTUAL_PORT=8000

// ...
version: '3.7'

services:
  // php

  angular:
    image: cocoders/phpers-summit-2019-angular
    volumes:
      - .:/var/www/beer
    networks:
      - default
      - nginx-proxy
    expose:
      - 4200
    environment:
      - VIRTUAL_HOST=frontend.beer.local
      - VIRTUAL_PORT=4200


// ...
version: '3.7'

services:
  // php and angular

  nginx-proxy:
    image: jwilder/nginx-proxy:alpine
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - default
      - nginx-proxy

  whoami:
    image: jwilder/whoami
    networks:
      - default
      - nginx-proxy

Dodajemy do /etc/hosts *

 

127.0.0.1 frontend.beer.local beer.local

Inicjalizacja projektu

* Na linuxach z obsługą make możemy wykonać polecenie

"sudo make init"

docker network create nginx-proxy

Inicjalizacja projektu

docker-compose up -d

Konfiguracja kluczy dla projektu firebase w Symfony

version: '3.7'

services:
  php:
    image: cocoders/phpers-summit-2019-symfony
    environment:
      - GOOGLE_APPLICATION_CREDENTIALS=/var/www/beer/key/key.json
    networks:
      - default
      - nginx-proxy
    volumes:
      - .:/var/www/beer
    expose:
      - 8000
    environment:
      - VIRTUAL_HOST=beer.local
      - VIRTUAL_PORT=8000

// ...

Zobaczmy composer.json

<?php

declare(strict_types=1);

namespace App\BeerMenu\Model;

final class Beer
{
    private string $name;
    private string $description;

    public function __construct(string $name, string $description)
    {
        $this->name = $name;
        $this->description = $description;
    }

    public function name(): string
    {
        return $this->name;
    }

    public function description(): string
    {
        return $this->description;
    }
}
<?php

declare(strict_types=1);

namespace App\BeerMenu\Model;

interface Beers
{
    public function inStock(Beer $beer): void;
    public function stockOut(string $beerName): void;
}
<?php

declare(strict_types=1);

namespace App\BeerMenu\Query;

interface CurrentMenuQuery
{
    public function count(): int;
}
<?php

declare(strict_types=1);

namespace App\BeerMenu\Infrastructure\Firestore;

use App\BeerMenu\Model\Beer;
use App\BeerMenu\Model\Beers as BeersInterface;
use Google\Cloud\Firestore\FirestoreClient;

final class Beers implements BeersInterface
{
    public function inStock(Beer $beer): void
    {
        $firestore = new FirestoreClient();
        $docRef = $firestore->collection('beers')->document($beer->name());
        $docRef->set([
            'name' => $beer->name(),
            'description' => $beer->description(),
        ]);
    }

    public function stockOut(string $beerName): void
    {
    }
}

symfony console make:command app:beer:add

Dodajmy do naszego polecenia użycie CurrentMenuQuery::count()

<?php

declare(strict_types=1);

namespace App\BeerMenu\Infrastructure\Firestore;

use App\BeerMenu\Query\CurrentMenuQuery as CurrentMenuQueryInterface;
use Google\Cloud\Firestore\FirestoreClient;

final class CurrentMenuQuery implements CurrentMenuQueryInterface
{
    public function count(): int
    {
        // implements this
        // hint: https://cloud.google.com/firestore/docs/query-data/get-data
    }
}

Zaimplementujmy

App\BeerMenu\Infrastructure\Firestore\Users::stockOut

<?php

declare(strict_types=1);

namespace App\BeerMenu\Infrastructure\Firestore;

use App\BeerMenu\Model\Beer;
use App\BeerMenu\Model\Beers as BeersInterface;
use Google\Cloud\Firestore\FirestoreClient;

final class Beers implements BeersInterface
{
    public function inStock(Beer $beer): void
    {
        $firestore = new FirestoreClient();
        $docRef = $firestore->collection('beers')->document($beer->name());
        $docRef->set([
            'name' => $beer->name(),
            'description' => $beer->description(),
        ]);
    }

    public function stockOut(string $beerName): void
    {
       // implement this
       // hint: https://cloud.google.com/firestore/docs/manage-data/delete-data
    }
}

symfony console make:command app:beer:remove

Implementacja Angular

npm install @angular/fire firebase --save

Konfiguracja w połączenia do firebase w pliku environment

export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};
import { Component, Input } from '@angular/core';
import { Beer } from '../beer';

@Component({
  selector: 'app-beer',
  templateUrl: './beer.component.html',
  styleUrls: ['./beer.component.scss']
})
export class BeerComponent {
  @Input() beer: Beer;
}
export interface Beer {
  name: string;
  description: string;
}
<ng-container *ngIf="beer">
  <figure>
    <img src="assets/beers/{{ beer.name | photoName }}.png"
        alt="{{ beer.description }}">
    <figcaption><h3>{{beer.name}}</h3></figcaption>
  </figure>
  <p>{{beer.description}}</p>
</ng-container>
import { Pipe, PipeTransform } from '@angular/core';
import { Beer } from '../beer';

@Pipe({
  name: 'photoName'
})
export class PhotoNamePipe implements PipeTransform {
  transform(value: string, ...args: any[]): any {
      if  (!value) {
        return '';
      }

      return value.replace(' ', '_').toLowerCase();
  }
}

Problemy

Trudne testowanie

Odcinamy infrastrukturę w testach

Pierwsza próba - tabelka w phpie

java -jar $HOME/.cache/firebase/emulators/cloud-firestore-emulator-v1.3.0.jar --host=127.0.0.1

Zmiana podejścia

Umieszczenie realtime w swojej architekturze

"Skakanie"

Reguły security

Dzięki!

Pora na piwo!