• Czas czytania ~10 min
  • 21.08.2022

Jak mogę to ująć? Aplikacje CLI są fajne. Możliwość otwarcia terminala w dowolnym miejscu i po prostu uruchomienia polecenia, aby wykonać zadanie, które mogło zająć znacznie więcej czasu. Otwarcie przeglądarki, przejście do właściwej strony, zalogowanie się i znalezienie tego, co należy zrobić, oraz oczekiwanie na załadowanie strony .... Otrzymujesz obraz.

W ciągu ostatnich kilku lat w terminal dowodzenia zainwestowano wiele;od ZSH do autouzupełniania, od FIG do Warp - CLI to coś, przed czym nie możemy uciec. Tworzę aplikacje CLI, aby pomóc mi być bardziej wydajnym przy małych zadaniach lub wykonywać pracę zgodnie z harmonogramem.

Zawsze kiedy patrzę online na cokolwiek związanego z Laravelem, zawsze jest to aplikację internetową i ma to sens. W końcu Laravel to fantastyczny framework aplikacji internetowych!Jednak wykorzystanie tego, co kochamy w Laravelu, jest również dostępne dla aplikacji CLI. Teraz możemy użyć pełnej instalacji Laravela i uruchomić harmonogram, aby uruchamiać polecenia rzemieślnika tam, gdzie jest to konieczne - ale czasami jest to przesada. Jeśli nie potrzebujesz interfejsu internetowego, nie potrzebujesz Laravela. Zamiast tego porozmawiajmy o Laravel Zero, kolejnym pomysłem Nuno Maduro.

Laravel Zero opisuje siebie jako „mikro-framework dla konsoli aplikacji” - co jest dość dokładne. Pozwala na budowanie aplikacji CLI przy użyciu sprawdzonego frameworka - czyli mniejszego niż użycie czegoś takiego jak Laravel.Jest dobrze udokumentowany, solidny i aktywnie utrzymywany - co czyni go idealnym wyborem dla dowolnych aplikacji CLI, które możesz chcieć zbudować.

W tym samouczku omówię nieco prosty przykład użycia Laravel Zero z nadzieją, że pokaże ci, jak bardzo może być użyteczny. Zbudujemy aplikację CLI, która pozwoli nam zobaczyć projekty i zadania w moim konto Todoist, dzięki czemu nie muszę otwierać aplikacji ani przeglądarki internetowej.

< p>Aby rozpocząć, musimy przejść do aplikacji internetowej Todoist i otworzyć ustawienia integracji, aby uzyskać nasz token API. Będziemy tego potrzebować trochę później. Naszym pierwszym krokiem jest stworzenie nowego projektu Laravel Zero, z którego możemy korzystać.

composer create-project --prefer-dist laravel-zero/laravel-zero todoist

Otwórz ten nowy projekt w swoim IDE, abyśmy mogli zacząć budować naszą aplikację CLI. Pierwszą rzeczą, o której wiemy, że będziemy chcieli zrobić, jest przechowywanie naszego tokena API, ponieważ nie chcemy wklejać go za każdym razem, gdy chcemy uruchomić nowe polecenie. Typowym podejściem jest tutaj przechowywanie tokena API w katalogu domowym użytkownika w pliku konfiguracyjnym w ukrytym katalogu.Przyjrzymy się więc, jak możemy to osiągnąć.

Chcemy utworzyć ConfigurationRepository, które pozwoli nam pracować z naszym lokalnym systemem plików aby uzyskać i ustawić wartości, których możemy potrzebować w naszej aplikacji CLI. Jak w przypadku większości kodu, który piszę, stworzę interfejs/kontrakt, który wiąże implementację na wypadek, gdybym chciał to zmienić, aby działał z innym systemem plików.

declare(strict_types=1);
 
namespace App\Contracts;
 
interface ConfigurationContract
{
    public function all(): array;
 
    public function clear(): ConfigurationContract;
 
    public function get(string $key, mixed $default = null): array|int|string|null;
 
    public function set(string $key, array|int|string $value): ConfigurationContract;
}

Teraz wiemy, co to powinno zrobić, możemy przyjrzeć się implementacji naszego lokalnego systemu plików:

declare(strict_types=1);
 
namespace App\Repositories;
 
use App\Contracts\ConfigurationContract;
use App\Exceptions\CouldNotCreateDirectory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
 
final class LocalConfiguration implements ConfigurationContract
{
    public function __construct(
        protected readonly string $path,
    ) {}
 
    public function all(): array
    {
        if (! is_dir(dirname(path: $this->path))) {
            if (! mkdir(
                    directory: $concurrentDirectory = dirname(
                        path: $this->path,
                    ),
                    permissions: 0755,
                    recursive: true
                ) && !is_dir(filename: $concurrentDirectory)) {
                throw new CouldNotCreateDirectory(
                    message: "Directory [$concurrentDirectory] was not created",
                );
            }
        }
 
        if (file_exists(filename: $this->path)) {
            return json_decode(
                json: file_get_contents(
                    filename: $this->path,
                ),
                associative: true,
                depth: 512,
                flags: JSON_THROW_ON_ERROR,
            );
        }
 
        return [];
    }
 
    public function clear(): ConfigurationContract
    {
        File::delete(
            paths: $this->path,
        );
 
        return $this;
    }
 
    public function get(string $key, mixed $default = null): array|int|string|null
    {
        return Arr::get(
            array: $this->all(),
            key: $key,
            default: $default,
        );
    }
 
    public function set(string $key, array|int|string $value): ConfigurationContract
    {
        $config = $this->all();
 
        Arr::set(
            array: $config,
            key: $key,
            value: $value,
        );
 
        file_put_contents(
            filename: $this->path,
            data: json_encode(
                value: $config,
                flags: JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT,
            ),
        );
 
        return $this;
    }
}

Używamy niektórych metod pomocniczych w Laravel i podstawowych PHP do pobierz zawartość i sprawdź pliki - a następnie przeczytaj i zapisz zawartość tam, gdzie jest to wymagane. Dzięki temu możemy zarządzać plikiem w dowolnym miejscu naszego lokalnego systemu plików.Naszym następnym krokiem jest powiązanie tego z naszym kontenerem, abyśmy mogli ustawić naszą bieżącą implementację i sposób, w jaki chcemy rozwiązać ten problem z kontenera.

declare(strict_types=1);
 
namespace App\Providers;
 
use App\Contracts\ConfigurationContract;
use App\Repositories\LocalConfiguration;
use Illuminate\Support\ServiceProvider;
 
final class AppServiceProvider extends ServiceProvider
{
    public array $bindings = [
        ConfigurationContract::class => LocalConfiguration::class,
    ];
 
    public function register(): void
    {
        $this->app->singleton(
            abstract: LocalConfiguration::class,
            concrete: function (): LocalConfiguration {
                $path = isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing'
                    ? base_path(path: 'tests')
                    : ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']);
 
                return new LocalConfiguration(
                    path: "$path/.todo/config.json",
                );
            },
        );
    }
}

Wiążemy naszą umowę na nasze wdrożenie przy użyciu właściwości bindings dostawcy usług tutaj. Następnie w metodzie register ustalamy, jak chcemy, aby nasza implementacja była zbudowana. Teraz, gdy wstrzykniemy ConfigurationContract do polecenia, otrzymamy instancję LocalConfiguration, która została rozwiązana jako singleton.

Pierwsza rzecz Chcemy teraz zrobić z aplikacją Laravel Zero, aby nadać jej nazwę, abyśmy mogli wywołać aplikację CLI przy użyciu nazwy, która jest odpowiednia dla tego, co budujemy. Zadzwonię do mojego „todo”.

php application app:rename todo

Teraz możemy wywołać nasze polecenie za pomocą php todo ... i zacząć tworzyć polecenia CLI, których będziemy chcieli użyć. Zanim zbudujemy polecenia, będziemy musieli stworzyć klasę, która integruje się z API Todoist. Ponownie, sporządzę na to interfejs/kontrakt, jeśli zdecyduję się przełączyć z Todoist na innego dostawcę.

declare(strict_types=1);
 
namespace App\Contracts;
 
interface TodoContract
{
    public function projects(): ResourceContract;
 
    public function tasks(): ResourceContract;
}

Mamy dwie metody, projekty i zadania, który zwróci klasę zasobów, z którą będziemy pracować. I jak zwykle ta klasa zasobów potrzebuje kontraktu. Kontrakt zasobu użyje kontraktu obiektu danych, ale zamiast go utworzyć, użyję tego, który wbudowałem w mój pakiet:

composer require juststeveking/laravel-data-object-tools

Teraz możemy utworzyć zasób Sam kontrakt:

declare(strict_types=1);
 
namespace App\Contracts;
 
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
interface ResourceContract
{
    public function list(): Collection;
 
    public function get(string $identifier): DataObjectContract;
 
    public function create(DataObjectContract $resource): DataObjectContract;
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract;
 
    public function delete(string $identifier): bool;
}

Są to podstawowe opcje CRUD dotyczące samego zasobu, nazwane pomocnie.Oczywiście możemy rozszerzyć to w implementacji, jeśli chcemy bardziej dostępnego API. Teraz zacznijmy budować naszą implementację Todoist.

declare(strict_types=1);
 
namespace App\Services\Todoist;
 
use App\Contracts\ResourceContract;
use App\Contracts\TodoContract;
use App\Services\Todoist\Resources\ProjectResource;
use App\Services\Todoist\Resources\TaskResource;
 
final class TodoistClient implements TodoContract
{
    public function __construct(
        public readonly string $url,
        public readonly string $token,
    ) {}
 
    public function projects(): ResourceContract
    {
        return new ProjectResource(
            client: $this,
        );
    }
 
    public function tasks(): ResourceContract
    {
        return new TaskResource(
            client: $this,
        );
    }
}

Opublikuję ten projekt na GitHub, aby umożliwić Ci zobaczenie pełnego działającego przykładu.

br>

Nasz TodoistClient zwróci nową instancję ProjectResourceprzekazanie instancji naszego klienta do konstruktora, abyśmy mogli uzyskać dostęp do adresu URL i tokena, dlatego te właściwości są chronione, a nie prywatne.

Spójrzmy na jak będzie wyglądał nasz ProjectResource. Następnie możemy przejść przez to, jak to będzie działać.

Całkiem prosta struktura, która dość dobrze pasuje do naszego interfejsu/umowy.Teraz możemy zacząć przyglądać się, jak chcemy tworzyć żądania i wysyłać je. Lubię to robić i nie krępuj się robić tego w inny sposób, polega na stworzeniu cechy, której mój zasób używa do wysyłania żądań. Następnie mogę ustawić tę nową metodę send na ResourceContract, aby zasoby albo korzystały z tej cechy, albo musiały zaimplementować własną metodę wysyłania.Interfejs API Todoist ma kilka zasobów, więc udostępnianie tego zachowania ma więcej sensu w ramach cechy. Spójrzmy na tę cechę:

declare(strict_types=1);
 
namespace App\Services\Todoist\Resources;
 
use App\Contracts\ResourceContract;
use App\Contracts\TodoContract;
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class ProjectResource implements ResourceContract
{
    public function __construct(
        private readonly TodoContract $client,
    ) {}
 
    public function list(): Collection
    {
        // TODO: Implement list() method.
    }
 
    public function get(string $identifier): DataObjectContract
    {
        // TODO: Implement get() method.
    }
 
    public function create(DataObjectContract $resource): DataObjectContract
    {
        // TODO: Implement create() method.
    }
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract
    {
        // TODO: Implement update() method.
    }
 
    public function delete(string $identifier): bool
    {
        // TODO: Implement delete() method.
    }
}

Mamy dwie metody, jedną, która zbuduje żądanie, a drugą, która wyśle ​​żądanie - ponieważ chcemy standardowego sposobu na wykonanie obu . Dodajmy teraz metodę send do ResourceContract, aby wymusić to podejście wśród dostawców.

declare(strict_types=1);
 
namespace App\Services\Concerns;
 
use App\Exceptions\TodoApiException;
use App\Services\Enums\Method;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
 
trait SendsRequests
{
    public function send(
        Method $method,
        string $uri,
        null|array $data = null,
    ): Response {
        $request = $this->makeRequest();
 
        $response = $request->send(
            method: $method->value,
            url: $uri,
            options: $data ? ['json' => $data] : [],
        );
 
        if ($response->failed()) {
            throw new TodoApiException(
                response: $response,
            );
        }
 
        return $response;
    }
 
    protected function makeRequest(): PendingRequest
    {
        return Http::baseUrl(
            url: $this->client->url,
        )->timeout(
            seconds: 15,
        )->withToken(
            token: $this->client->token,
        )->withUserAgent(
            userAgent: 'todo-cli',
        );
    }
}

Teraz nasze zasoby albo muszą stworzyć własny sposób tworzenia i wysyłania żądania, albo mogą zaimplementować tę cechę. Jak widać na przykładach kodu, stworzyłem pomocnika Enum dla metody request – ten kod znajduje się w repozytorium, więc zachęcamy do zagłębienia się w ten kod, aby uzyskać więcej informacji.

Zanim zajdziemy za daleko ze stroną integracyjną, chyba czas stworzyć komendę do logowania. W końcu ten samouczek dotyczy Laravel Zero!

declare(strict_types=1);
 
namespace App\Contracts;
 
use App\Services\Enums\Method;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
interface ResourceContract
{
    public function list(): Collection;
 
    public function get(string $identifier): DataObjectContract;
 
    public function create(DataObjectContract $resource): DataObjectContract;
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract;
 
    public function delete(string $identifier): bool;
 
    public function send(
        Method $method,
        string $uri,
        null|array $data = null,
    ): Response;
}

Utwórz nowe polecenie za pomocą następującego polecenia w swoim terminalu:

To polecenie będzie musiało pobrać token API i zapisać go w repozytorium konfiguracji dla przyszłych poleceń. Zobaczmy, jak działa to polecenie:

Wstawiamy ConfigurationContract do metody handle, która rozwiąże dla nas konfigurację. Następnie prosimy o token API jako sekret, aby nie był wyświetlany na terminalu użytkownika podczas wpisywania. Po wyczyszczeniu wszelkich bieżących wartości możemy użyć konfiguracji, aby ustawić nowe wartości dla tokena i adresu URL.

php todo make:command Todo/LoginCommand

Po uwierzytelnieniu możemy utworzyć dodatkowe polecenie, aby wyświetlić listę naszych projektów. Stwórzmy to teraz:

declare(strict_types=1);
 
namespace App\Commands\Todo;
 
use App\Contracts\ConfigurationContract;
use LaravelZero\Framework\Commands\Command;
 
final class LoginCommand extends Command
{
    protected $signature = 'login';
 
    protected $description = 'Store your API credentials for the Todoist API.';
 
    public function handle(ConfigurationContract $config): int
    {
        $token = $this->secret(
            question: 'What is your Todoist API token?',
        );
 
        if (! $token) {
            $this->warn(
                string: "You need to supply an API token to use this application.",
            );
 
            return LoginCommand::FAILURE;
        }
 
        $config->clear()->set(
            key: 'token',
            value: $token,
        )->set(
            key: 'url',
            value: 'https://api.todoist.com/rest/v1',
        );
 
        $this->info(
            string: 'We have successfully stored your API token for Todoist.',
        );
 
        return LoginCommand::SUCCESS;
    }
}

To polecenie będzie musiało użyć TodoistClient, aby pobrać wszystkie projekty i umieścić je w tabeli. Zobaczmy, jak to wygląda.

Jeśli spojrzysz na kod w repozytorium na GitHub, zobaczysz, że lista polecenie na ProjectResource zwraca kolekcję obiektów danych Project. Dzięki temu możemy odwzorować każdy element w kolekcji, rzutować obiekt na tablicę i zwrócić kolekcję jako tablicę, dzięki czemu możemy łatwo zobaczyć, jakie mamy projekty w formacie tabelarycznym. Korzystając z odpowiedniego terminala, możemy również kliknąć adres URL projektu, aby otworzyć go w przeglądarce, jeśli zajdzie taka potrzeba.

php todo make:command Todo/Projects/ListCommand

Jak widać z powyższego podejścia, zbudowanie aplikacji CLI przy użyciu Laravel Zero jest dość proste - jedynym ograniczeniem tego, co możesz zbudować, jest Twoja wyobraźnia.

declare(strict_types=1);
 
namespace App\Commands\Todo\Projects;
 
use App\Contracts\TodoContract;
use App\DataObjects\Project;
use LaravelZero\Framework\Commands\Command;
use Throwable;
 
final class ListCommand extends Command
{
    protected $signature = 'projects:list';
 
    protected $description = 'List out Projects from the Todoist API.';
 
    public function handle(
        TodoContract $client,
    ): int {
        try {
            $projects = $client->projects()->list();
        } catch (Throwable $exception) {
            $this->warn(
                string: $exception->getMessage(),
            );
 
            return ListCommand::FAILURE;
        }
 
        $this->table(
            headers: ['ID', 'Project Name', 'Comments Count', 'Shared', 'URL'],
            rows: $projects->map(fn (Project $project): array => $project->toArray())->toArray(),
        );
 
        return ListCommand::SUCCESS;
    }
}

Jak wspomniano w tym samouczku, możesz znaleźć Repozytorium GitHub online tutaj, dzięki czemu możesz sklonować kompletny przykład pracy.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297