W dzisiejszym wpisie chciałbym przedstawić jeden z prostszych sposobów, w jaki można odseparować od siebie fragmenty kodu w projekcie. Sposób ten będzie opierał się na paczkach ładowanych popularnym (w środowisku programistów PHP) menedżerem zależności Composer.

Kilka słów wstępu

W rzeczywistych rozwiązaniach oprogramowanie jest tworzone z gotowych komponentów. Większość firm na rynku nie osiągnęłaby sukcesu, gdyby miała stworzyć wszystkie komponenty swojego oprogramowania własnoręcznie. Powód tego jest prosty - wymagałoby to zbyt dużej ilości czasu oraz pracowników. A obu z tych rzeczy w każdej firmie brakuje :(. Dlatego najczęściej bazuje się na gotowych, uniwersalnych komponentach, do których dodaje się własną, unikatową wartość biznesową.

Jednym z najczęściej wykorzystywanych zewnętrznych komponentów są tak zwane frameworki, których zadaniem jest rozwiązanie podstawowych problemów, które występują podczas rozpoczynania projektu. Przykładowo, w języku PHP mamy ciekawy framework Laravel, który zajmuje się kompleksową obsługą wejścia-wyjścia. Wejście w tym przypadku rozumiem jako żądanie wysyłane do serwera, zawierające wprowadzone od użytkownika dane, natomiast wyjście rozumiem jako odpowiedź serwera (w rezultacie na ekran komputera) po wykonaniu zaprogramowanych przez programistę operacji. Dzięki wykorzystaniu tego komponentu programista musi zająć się tutaj wyłącznie obróbką danych.

Laravel, tak jak wiele innych komponentów w języku PHP, pobierany jest do projektu dzięki Composerowi. Jak wygląda praca z Composerem? Po stworzeniu w katalogu projektu pliku composer.json (ręcznie albo za pomocą polecenia composer init), dodaniu do wymaganych zależności odpowiednich paczek, a następnie uruchomieniu komendy composer update pliki źródłowe pobierane są do katalogu vendor. Paczki te są pobierane głównie z serwera packagist, gdzie znajdują się udostępnione przez różne osoby paczki.

Tworząc prywatny projekt, chociażby hobbistycznie, także możemy tworzyć takie zależności, które będą pobierane przez Composera. Nie ma tutaj przymusu umieszczania kodu na ogólnodostępnym serwerze packagist. Teraz chciałbym pokazać jak to zrobić, obrazując to na prostym przykładzie. Będzie to skrypt, który będzie pobierał zależności z innego, niezależnego projektu - w tym przypadku prostego wrappera na tablice dostępne w języku PHP.

Tworzymy niezależny komponent

Najpierw tworzymy pusty projekt, w którym dodajemy katalog src na pliki źródłowe oraz plik composer.json z konfiguracją:

{ "name": "rombarte/array-of-type", "type": "library", "description": "Helper class for standard arrays", "minimum-stability": "stable", "license": "MIT", "authors": [ { "name": "Bartłomiej Romanek", "email": "b.romanek@rombarte.pl" } ], "require": {}, "autoload": { "psr-4": { "Rombarte\\ArrayOfType\\": "src/" } } }

Co jest tutaj ciekawego? Otóż tak jak ci się wydaje - nic ;). Oprócz tego, że typ projektu oznaczyłem jako library, plik composer.json nie różni się niczym szczególnym od typowej konfiguracji w projektach, z którymi pracujemy na co dzień. Dodaję do projektu także pliki źródłowe:

W katalogu src/Components plik ArrayOfType.php z zawartością:

<?php declare(strict_types=1); namespace Rombarte\ArrayOfType\Components; use Rombarte\ArrayOfType\ArrayOfTypeInterface; use Rombarte\ArrayOfType\Exceptions\UnexpectedTypeException; class ArrayOfType implements ArrayOfTypeInterface { private const ALLOWED_ELEMENTS_TYPES = [ 'bool', 'boolean', 'float', 'int', 'integer', 'string', ]; /** @var string */ private $type; /** @var array */ private $array; public function __construct() { $this->array = []; } /** * Set array elements type * @param string $type * @return ArrayOfTypeInterface * @throws UnexpectedTypeException */ public function setType(string $type): ArrayOfTypeInterface { if (!in_array($type, self::ALLOWED_ELEMENTS_TYPES)) { throw new UnexpectedTypeException('Unexpected elements type: ' . $type); } $this->type = $type; return $this; } /** * Whether a offset exists * @param mixed $offset * @return boolean true on success or false on failure. * The return value will be casted to boolean if non-boolean was returned. * @since 5.0.0 */ public function offsetExists($offset) { return isset($this->array[$offset]); } /** * Offset to retrieve * @param mixed $offset * @return mixed Can return all value types. * @since 5.0.0 */ public function offsetGet($offset) { return $this->array[$offset]; } /** * Offset to set * @param mixed $offset * @param mixed $value * @return void * @since 5.0.0 * @throws UnexpectedTypeException */ public function offsetSet($offset, $value) { $paramType = gettype($value); if ($paramType !== $this->type) { throw new UnexpectedTypeException('Unexpected elements type: ' . $paramType); } if (empty($offset)) { $this->array[] = $value; } else { $this->array[$offset] = $value; } } /** * Offset to unset * @param mixed $offset * @return void * @since 5.0.0 */ public function offsetUnset($offset) { unset($this->array[$offset]); $this->array = array_values($this->array); } }

W katalogu src/Exceptions plik UnexpectedTypeException z zawartością:

<?php declare(strict_types=1); namespace Rombarte\ArrayOfType\Exceptions; class UnexpectedTypeException extends \Exception {}

Na koniec w katalogu src plik ArrayOfTypeInterface.php z zawartością:

<?php declare(strict_types=1); namespace Rombarte\ArrayOfType; interface ArrayOfTypeInterface extends \ArrayAccess { /** * Set array elements type * @param string $type * @return ArrayOfTypeInterface */ public function setType(string $type): ArrayOfTypeInterface; }

Powyższy komponent (składający się tak naprawdę z jednej klasy) zabezpiecza przed dodawaniem do tablicy elementów o typie innym, niż zdefiniowany po wywołaniu metody setType.

Tak przygotowany komponent jest gotowy do załączenia w innym projekcie. Aby to zrobić, należy projekt umieścić w repozytorium git. Jego położenie i dostawca nie ma znaczenia - ja umieściłem projekt na Bitbuckecie.

Tworzymy projekt z zależnością

Teraz tworzymy prosty projekt, w którym wykorzystamy utworzoną wcześniej zależność. Tworzymy plik composer.json z zawartością:

{ "name": "rombarte/my-project", "description": "My project with my external composer package", "type": "project", "license": "MIT", "minimum-stability": "stable", "authors": [ { "name": "Bartłomiej Romanek", "email": "b.romanek@rombarte.pl" } ], "require": { "php": "^7.2", "rombarte/array-of-type": "dev-master" }, "autoload": { "psr-4": { "ComposerPackage\\": "src/" } }, "repositories": [ { "type": "git", "url": "https://rombarte@bitbucket.org/rombarte/array-of-type.git" } ] }

Własne zależności definiujemy w kluczach require / require-dev, tak jak to ma miejsce w paczkach pobieranych z oficjalnego repozytorium. Natomiast url do naszego repozytorium podajemy w kluczu repositories. I to tyle! Ciekawą kwestią jest nazwa brancha zamiast numeru wersji. W podstawowej konfiguracji repozytorium z paczką dla Composera podajemy nazwę brancha z przedrostkiem dev-. Podczas wykonywania composer update Composer będzie aktualizował za każdym razem pakiet, jeżeli zmieni się hash commita na aktualnym branchu. Można wprowadzić także normalne wersjonowanie, ale wymaga to odpowiedniego przygotowania branchy w repozytorium.

Teraz wykonujemy polecenie composer update. Jak możesz zauważyć, w katalogu vendor pojawił się dodatkowy katalog z plikami utworzonej wcześniej paczki (struktura katalogów została zachowana).

Uwaga! Jeżeli korzystasz z prywatnego repozytorium, to composer poprosi Cię o hasło, aby móc pobrać zależności.

Następnie tworzymy plik index.php, w którym wykorzystujemy pobraną zależność:

<?php require_once "./vendor/autoload.php"; $array1 = new \Rombarte\ArrayOfType\Components\ArrayOfType(); $array1->setType('boolean'); $array1[] = true; $array1[] = false; var_dump($array1);

Podsumowanie

Composer jest bardzo uniwersalnym narzędziem i pozwala na zarządzanie zależnościami różnego pochodzenia. Warto tworzyć paczki ładowane przez menedżera zależności, ponieważ wymusza to na programiście stawianie wyraźnych granic między komponentami. Zachęcam cię do dalszego eksperymentowania i zapoznania się z innymi funkcjami Composera - uważam, że warto. Kod źródłowy z tego wpisu możesz pobrać tutaj.