Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 5]

in polish •  7 years ago  (edited)

email-marketing-2362038_640.png

Czego nauczę się w tym poradniku?

  • Jak utworzyć przyjazną konfigurację dla modułu
  • Jak odwołać się do interfejsu podczas mapowania powiązań

Wymagania

  • System z rodziny UNIX/Linux
  • Serwer Apache2 z PHP w wersji 7.1
  • Baza danych MySQL
  • Projekt Symfony pobrany stąd
  • Dowolny edytor tekstowy
  • Istniejący projekt w Firebase
  • Narzędzie composer

Poziom trudności

  • Średni

Treść poradnika

Poradnik jest kolejnym z serii Web Development, gdzie opisywane są procesy budowy aplikacji serwerowej w Symfony, zdolnej do wysyłki powiadomień push na urządzenia mobilne. W poprzednim artykule przedstawiony został proces tworzenia mechanizmu uwierzytelniającego, pozwalającego na zabezpieczenie RESTful API przed nieautoryzowanym dostępem, za pomocą klucza api key.

Przykładowy projekt wykorzystywany na potrzeby tego poradnika znajduje się tutaj. Jest to w zasadzie kod, który otrzymamy po ukończeniu poprzedniego kompendium. Wskazanym jest pobranie wspomnianego projektu z repozytorium github.

Jakie aspekty Web Development z Symfony zostaną przedstawione w tym poradniku?

  • Proces tworzenia przyjaznej konfiguracji dla modułu za pomocą komponentu Config, pozwalającego na definiowanie parametrów konfiguracyjnych, które mogą zostać wykorzystane przez framework Symfony do wprowadzania złożonych zmian w aplikacji.
  • Proces tworzenia mapowania powiązań pomiędzy encjami za pomocą odwołań do interfejsów zamiast do klas właściwych, pozwalający na uniknięcie sztywnych relacji pomiędzy klasami.

Jak utworzyć przyjazną konfigurację dla modułu?

Definiując konfigurację dla modułu za pomocą komponentu Config , proces ustawienia podstawowych parametrów można scentralizować, ze względu na fakt, że wszelkie wartości są ustawiane w jednym pliku konfiguracyjnym, dedykowanym dla konkretnego modułu. Dodatkowo wartości te mogą zostać wykorzystane jako parametry kontenera, co z kolei pozwala usprawnić proces definiowania usług.

Plik src/Ptrio/MessageBundle/DependencyInjection/Configuration.php pozwala na zdefiniowanie wielopoziomowej tablicy tablic, której poszczególne elementy mogą zostać wykorzystane do skonfigurowania modułu. Obiekt typu TreeBuilder pozwana na m.in. ustawienie domyślnych wartości dla danego modułu oraz umożliwia weryfikację wszelkich wprowadzonych pozycji, jednocześnie zapobiegając umieszczeniu nieprawidłowych wpisów w pliku konfiguracyjnym.

Drzewo konfiguracyjne tworzone jest w metodzie Configuration::getConfigTreeBuilder().

<?php
// src/Ptrio/MessageBundle/DependencyInjection/Configuration.php
namespace App\Ptrio\MessageBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('ptrio_message');
        // Some logic responsible for building a configuration tree...
        return $treeBuilder;
    }
}

Jak widać na powyższym przykładzie, nadrzędny węzeł drzewa konfiguracyjnego powinien zostać skonfigurowany w pierwszej kolejności. ptrio_message to alias modułu, który składa się z nazwy wydawcy oraz nazwy konkretnego modułu, oddzielonych znakiem podkreślenia.

Definicja drzewa konfiguracyjnego

Na tę chwilę moduł PtrioMessageBundle posiada zdefiniowane wartości pozwalające na konfigurację klienta Firebase.

        // src/Ptrio/MessageBundle/DependencyInjection/Configuration.php
        $rootNode
            ->children()
                ->arrayNode('firebase')
                    ->children()
                        ->scalarNode('api_url')->isRequired()->cannotBeEmpty()->end()
                        ->scalarNode('server_key')->isRequired()->cannotBeEmpty()->end()
                    ->end()
                ->end()
            ->end()
        ;

Tablica uzyskana z pomocą przedstawionego drzewa zaprezentowana jest poniżej.

[
    [
        'firebase' => [
            'api_url' => 'firebase_api_base_url',
            'server_key' => 'firebase_project_server_key',
        ],
    ],
]

Celem kolejnego kroku, jest takie przebudowanie konfiguracji dla modułu PtrioMessageBundle aby pełna nazwa klasy encji urządzenia przekazywana jako argument konstruktora usługi managera urządzeń mogła być definiowana w pliku konfiguracyjnym modułu config/packages/ptrio_message.yaml.

Uprzednio pełna nazwa klasy encji urządzenia została wyizolowana do parametru kontenera ptrio_message.model.device.class, który został zdefiniowany w pliku konfiguracji usług services.yaml. Minusem takiego rozwiązania jest niemożność wykorzystania tego parametru podczas procesu inicjalizacji modułu w projekcie oraz brak możliwości dodania walidacji dla wartości umieszczonej we wspomnianym parametrze.

Do do nadrzędnego węzła obiektu typu TreeBuilder należy dodać poniższą definicję.

                ->arrayNode('device')
                    ->children()
                        ->arrayNode('classes')
                            ->children()
                                ->scalarNode('model')->isRequired()->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()

Jak widać na przykładzie powyżej, parametr zawierający nazwę klasy jest wymagany i nie może być pusty.

Uzyskana tablica będzie wyglądać podobnie do tej, która znajduje się poniżej.

[
    [
        'device' => [
            'classes' => [
                'model' => 'App\Ptrio\MessageBundle\Entity\Device',
            ]
        ]
    ]
]

Tablica zdefiniowana z pomocą obiektu TreeBuilder jest przekazywana jako pierwszy argument metody Extension::load(array $configs, ContainerBuilder $container)i może teraz zostać użyta do konfiguracji modułu.

Ładowanie konfiguracji dla modułu

Aby utrzymać porządek w klasie PtrioMessageExtension odpowiedzialnej za załadowanie konfiguracji modułu, wskazane jest wyizolowanie logiki związanej z obiektem urządzenia do osobnej metody.

    /**
     * @param array $config
     * @param ContainerBuilder $container
     */
    private function loadDevice(array $config, ContainerBuilder $container)
    {
        $container->setParameter('ptrio_message.model.device.class', $config['classes']['model']);
    }

Następnie powyższa metoda może zostać wywołana w metodzie PtrioMessageExtension::load(array $configs, ContainerBuilder $container).

    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
        $loader->load('services.yaml');
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);
        $container->setParameter('ptrio_message.firebase.api_url', $config['firebase']['api_url']);
        $container->setParameter('ptrio_message.firebase.server_key', $config['firebase']['server_key']);
        $this->loadDevice($config['device'], $container); // add this line
    }

Teraz wartość dla parametru ptrio_message.model.device.class może zostać zdefiniowana w pliku config/packages/ptrio_message.yaml.

    device:
        classes:
            model: 'App\Ptrio\MessageBundle\Entity\Device'

Na koniec można usunąć definicję parametru ptrio_message.model.device.class z pliku src/Ptrio/MessageBundle/Resources/config/services.yaml, gdyż nie będzie ona już potrzebna.

Informacja: Wskazane jest powtórzenie powyższych czynności dla konfiguracji związanej z obiektami wiadomości i urządzenia.

Jak odwołać się do interfejsu podczas mapowania powiązań?

Dotychczas, podjęte zostały kroki, które pozwoliły na uniknięcie sztywnych referencji do klas encji urządzeń, wiadomości i użytkowników. Pozostała jednak jedna definicja zawierająca odwołanie do klasy właściwej obiektu urządzenia w klasie encji Message.

Biblioteka Doctrine umożliwia zastąpienie referencji do klasy właściwej, klasą abstrakcyjną lub interfejsem, dzięki zastosowaniu obiektu typu ResolveTargetEntityListener, który odpowiedzialny jest za zastąpienie wartości parametru targetEntity na wartość docelową, podczas inicjalizacji danego modułu.

Aktualizacja drzewa konfiguracyjnego

W pierwszym kroku, należy zaktualizować drzewo konfiguracyjne modułu PtrioMessageBundle, aby umożliwić definicję interfejsu, który zastąpiony będzie klasą właściwą encji.

W pliku src/Ptrio/MessageBundle/DependencyInjection/Configuration.php, do węzła classes należy dodać węzeł interface.

                        ->arrayNode('classes')
                            ->children()
                                ->scalarNode('model')->isRequired()->cannotBeEmpty()->end()
                                ->scalarNode('interface')->cannotBeEmpty()->defaultValue('App\Ptrio\MessageBundle\Model\DeviceInterface')->end()
                            ->end() // add this line

Domyślną wartością dla węzła interface będzie App\Ptrio\MessageBundle\Model\DeviceInterface.

Aktualizacja klasy Extension

Klasę PtrioMessageExtension należy zaktualizować tak, aby obiekt-słuchacz typu ResolveTargetEntityListener otrzymał informację na temat tego, jaki interfejs powinien zostać zastąpiony daną klasą właściwą.

W pierwszym kroku, klasa PtrioMessageExtension musi implementować interfejs Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface.

class PtrioMessageExtension extends Extension implements PrependExtensionInterface

Następnie, do wspomnianej klasy, konieczne będzie dodanie metody prepend(ContainerBuilder $container), umożliwiającej rozszerzenie konfiguracji dowolnego, innego modułu. W tym przypadku rozszerzona będzie konfiguracja modułu DoctrineBundle.

    public function prepend(ContainerBuilder $container)
    {
        $config = $this->processConfiguration(new Configuration(), $container->getExtensionConfig($this->getAlias()));
        $bundles = $container->getParameter('kernel.bundles');
        if (isset($bundles['DoctrineBundle'])) {
            $container
                ->loadFromExtension('doctrine', [
                    'orm' => [
                        'resolve_target_entities' => [
                            $config['device']['classes']['interface'] => $config['device']['classes']['model'],
                        ],
                    ],
                ])
            ;
        }
    }

Omówmy teraz po krótce co dzieje się wewnątrz metody PtrioMessageExtension::prepend(ContainerBuilder $container).

W pierwszej kolejności konfiguracja dla modułu PtrioMessageBundle wczytywana jest za pomocą metody ContainerBuilder::getExtensionConfig($name). Następnie jest ona przetwarzana z zastosowaniem metody Extension::processConfiguration(ConfigurationInterface $configuration, array $configs), aby mogły zostać ustawione wartości domyślne zdefiniowane w konfiguracji.

Następnie z obiektu typu ContainerBuilder wyciągany jest parametr kernel.bundles, który zawiera listę modułów załadowanych w projekcie.

W kolejnym kroku, algorytm sprawdza czy wśród załadowanych modułów znajduje się moduł DoctrineBundle. Jeżeli tak, to jego konfiguracja rozszerzana jest o definicję interfejsu, który ma być zastąpiony klasą właściwą przez obiekt typu ResolveTargetEntityListener.

Aktualizacja mapowania w klasie encji wiadomości

Ostatnią czynnością jest zmiana wartości parametru targetEntity w mapowaniu relacyjnym dla własności Message::$device, z klasy właściwej na interfejs.

    // src/Ptrio/MessageBundle/Entity/Message.php
    /**
     * @ORM\ManyToOne(targetEntity="App\Ptrio\MessageBundle\Model\DeviceInterface")
     * @ORM\JoinColumn(name="device_id", referencedColumnName="id")
     */
    protected $device;

Curriculum

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

@resteemator is a new bot casting votes for its followers. Follow @resteemator and vote this comment to increase your chance to be voted in the future!

Congratulations! This post has been upvoted from the communal account, @minnowsupport, by piotr42 from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.

If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.