• Czas czytania ~5 min
  • 31.08.2022

Czasami musisz pracować z poleceniami na poziomie systemu operacyjnego z aplikacji PHP. Zobaczmy, jak możemy to zrobić i zobaczmy, czy możemy ulepszyć środowisko programisty.

W ciągu ostatnich kilku lat skupiałem się na różnych aspektach Piszę kod i jak mogę go poprawić. Zacząłem od sprawdzenia, w jaki sposób mogę sprawić, by integracja z HTTP była lepsza i bardziej zorientowana obiektowo.Wierzę, że znalazłem sposób, aby to osiągnąć i teraz skupiam swoją uwagę gdzie indziej.

Są sytuacje, w których chcesz pracować z OS CLI w swoich aplikacjach. Albo w aplikacji internetowej, albo w innej aplikacji CLI. W przeszłości używaliśmy metod takich jak exec lub passthru lub shell_exec i system.Potem pojawił się komponent Symfony Process i zostaliśmy uratowani.

Komponent procesu Symfony sprawił, że integracja z procesami systemu operacyjnego i uzyskiwanie danych wyjściowych była bardzo łatwa. Ale to, jak integrujemy się z tą biblioteką, wciąż jest trochę frustrujące. Tworzymy nowy proces, przekazując tablicę argumentów tworzących polecenie, które chcemy uruchomić. Rzućmy okiem:

$command = new Process(
	command: ['git', 'push', 'origin', 'main'],
);
 
$command->run();

Co jest złego w tym podejściu? Cóż, szczerze mówiąc, nic. Ale czy jest jakiś sposób na poprawę doświadczenia programistów? Załóżmy, że przechodzimy z git na svn (mało prawdopodobne, że wiem).

Aby poprawić wrażenia programisty, najpierw musimy zrozumieć komponenty, które logicznie wchodzą w tworzenie polecenia systemu operacyjnego. Możemy je podzielić na:

executable
arguments

Nasz plik wykonywalny to coś, z czym wchodzimy w bezpośrednią interakcję, na przykład php, git, brew lub jakikolwiek inny plik binarny zainstalowany w naszym systemie. Następnie argumenty dotyczą tego, jak możemy wchodzić w interakcje; mogą to być podkomendy, opcje, flagi lub argumenty.

Jeśli więc trochę abstrahujemy, będziemy mieli proces i < code>polecenie, które pobiera argumenty.Użyjemy interfejsów/umów, aby zdefiniować nasze komponenty, aby kontrolować, jak powinien działać nasz przepływ pracy. Zacznijmy od Kontraktu Procesowego:

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
use Symfony\Component\Process\Process;
 
interface ProcessContract
{
	public function build(): Process;
}

Mówimy tutaj, że każdy proces musi być możliwy do zbudowania, a wynikiem stworzonego procesu powinien być proces Symfony. Nasz proces powinien zbudować polecenie do uruchomienia, więc teraz spójrzmy na nasz kontrakt poleceń:

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
interface CommandContract
{
	public function toArgs(): array;
}

Najważniejszą rzeczą, jakiej oczekujemy od naszego polecenia, jest możliwość zwrócenia ich jako argumentów, które możemy przekazać do procesu Symfony jako polecenie.

Dość już pomysłów, przejdźmy przez prawdziwy przykład. Użyjemy git jako przykładu, ponieważ większość z nas powinna być w stanie odnieść się do poleceń git.

Najpierw stwórzmy proces Git, który implementuje kontrakt na proces które właśnie opisaliśmy:

class Git implements ProcessContract
{
	use HandlesGitCommands;
 
	private CommandContract $command;
}

Nasz proces realizuje kontrakt i posiada właściwość polecenia, której użyjemy, aby umożliwić płynne zbudowanie i wykonanie naszego procesu. Mamy cechę, która pozwoli nam scentralizować sposób, w jaki rzeczy są budowane i tworzone dla naszego procesu Git. Przyjrzyjmy się temu:

trait HandlesGitCommands
{
	public function build(): Process
	{
		return new Process(
			command: $this->command->toArgs(),
		);
	}
 
	protected function buildCommand(Git $type, array $args = []): void
	{
		$this->command = new GitCommand(
			type: $type,
			args: $args,
		);
	}
}

Tak więc nasza cecha pokazuje samą implementację kontraktu procesowego i dostarcza instrukcji, jak procesy powinny być budowane. Zawiera również metodę pozwalającą nam na abstrakcyjne polecenia budowania.

Możemy stworzyć proces i zbudować potencjalne polecenie do tego momentu. Jednak musimy jeszcze wydać polecenie. Tworzymy nową komendę Git w cesze, która używa klasy Git dla typu.Spójrzmy na tę inną klasę Git, która jest enum. Pokażę jednak okrojoną wersję - tak realistycznie, że chcesz, aby było to mapowane na wszystkie podkomendy git, które chcesz obsługiwać:

enum Git: string
{
	case PUSH = 'push';
	case COMMIT = 'commit';
}

Następnie przekazujemy to przez do polecenia Git:

final class GitCommand implements CommandContract
{
	public function __construct(
		public readonly Git $type,
		public readonly array $args = [],
		public readonly null|string $executable = null,
	) {
	}
 
	public function toArgs(): array
	{
		$executable = (new ExecutableFinder())->find(
			name: $this->executable ?? 'git',
		);
 
		if (null === $executable) {
			throw new InvalidArgumentException(
				message: "Cannot find executable for [$this->executable].",
			);
		}
 
		return array_merge(
			[$executable],
			[$this->type->value],
			$this->args,
		);
	}
}

W tej klasie akceptujemy argumenty z naszego procesu, który jest obecnie obsługiwany przez naszą cechę HandledGitCommands.Następnie możemy przekształcić to w argumenty zrozumiałe dla procesu Symfony. Używamy ExecutableFinder z pakietu Symfony, aby zminimalizować błędy w ścieżkach. Jednak chcemy również zgłosić wyjątek, jeśli nie można znaleźć pliku wykonywalnego.

Kiedy połączymy to wszystko w naszym procesie Git, wygląda to trochę tak:

use JustSteveKing\OS\Commands\Types\Git as SubCommand;
 
class Git implements ProcessContract
{
	use HandlesGitCommands;
 
	private CommandContract $command;
 
	public function push(string $branch): Process
	{
		$this->buildCommand(
			type: SubCommand:PUSH,
			args: [
				'origin',
				$branch,
			],
		);
 
		return $this->build();
	}
}

Teraz pozostaje nam tylko uruchomić sam kod, abyśmy mogli ładnie pracować z git wewnątrz naszej aplikacji PHP:

$git = new Git();
$command = $git->push(
	branch: 'main',
);
 
$result = $command->run();

Wynik metody push pozwoli ci na interakcję z procesem Symfony - co oznacza, że ​​możesz robić wszystko za pomocą polecenia po drugiej stronie. Jedyne, co zmieniliśmy, to zbudowanie wrappera zorientowanego obiektowo wokół tworzenia tego procesu.To pozwala nam ładnie rozwijać i utrzymywać kontekst oraz rozszerzać rzeczy w testowalny i rozszerzalny sposób.

Jak często pracujesz z poleceniami systemu operacyjnego w swoich aplikacjach? Czy możesz wymyślić jakieś przypadki użycia tego? opublikowałem przykładowy kod w repozytorium na GitHub, abyś mógł go użyć i zobaczyć, czy możesz poprawić integrację systemu operacyjnego .

Doskonałym tego przykładem powinien być SSH, MySQL, a nawet ansible lub terraform! Wyobraź sobie, że możesz efektywnie uruchamiać zrzuty MySQL zgodnie z harmonogramem od Laravel artisan bez ciągłego korzystania z pakietów innych firm!

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