From 878fe0ab770d4059189329459f5eb263e2443978 Mon Sep 17 00:00:00 2001 From: Anton Stepanov Date: Wed, 5 Apr 2023 08:37:40 +0300 Subject: [PATCH] [16] Architecture patterns. --- .env.dev.dist | 16 +++ .gitignore | 26 ++++ Makefile | 30 +++++ README.md | 6 +- app/bin/console | 21 +++ app/composer.json | 73 +++++++++++ app/config/services.yaml | 38 ++++++ app/exceptions/Assert/Assert.php | 18 +++ .../Assert/FileStorage/Directory.php | 18 +++ app/exceptions/Assert/Regexp/RegexpMatch.php | 18 +++ app/exceptions/UnexpectedValueException.php | 9 ++ app/phpstan.neon | 15 +++ app/phpunit.xml.dist | 18 +++ app/psalm.xml | 63 +++++++++ app/src/Application.php | 23 ++++ app/src/Domain/Comparator/Comparison.php | 38 ++++++ .../Comparator/Contract/CompareInterface.php | 14 ++ .../FileSize/DirectorySizeComparator.php | 37 ++++++ .../FileSize/FileSizeComparator.php | 37 ++++++ app/src/Domain/DirectoryTree/Builder.php | 62 +++++++++ .../Contract/BuilderInterface.php | 21 +++ .../DirectoryTree/Directory/Content.php | 23 ++++ .../Directory/DirectoryFirstSorter.php | 20 +++ .../File/Contract/FileInterface.php | 20 +++ app/src/Domain/DirectoryTree/File/Factory.php | 35 +++++ .../DirectoryTree/Filter/SizeFilter.php | 46 +++++++ app/src/Domain/DirectoryTree/Iterator.php | 71 ++++++++++ .../DirectoryTree/Mime/GioInfoTypeGuesser.php | 82 ++++++++++++ .../DirectoryTree/Node/AbstractNode.php | 38 ++++++ .../Domain/DirectoryTree/Node/Directory.php | 40 ++++++ app/src/Domain/DirectoryTree/Node/File.php | 38 ++++++ app/src/Domain/DirectoryTree/Size/Size.php | 41 ++++++ app/src/Domain/DirectoryTree/Size/Unit.php | 28 ++++ .../Cli/DirectoryTree/Command/Command.php | 53 ++++++++ .../View/Contract/DirectoryTreeInterface.php | 16 +++ .../View/Contract/PresentationInterface.php | 18 +++ .../Contract/PresentationTagInterface.php | 16 +++ .../View/Directory/Presenter.php | 28 ++++ .../Cli/DirectoryTree/View/DirectoryTree.php | 28 ++++ .../Contract/ContentFetcherInterface.php | 15 +++ .../Contract/ContentTagFetcherInterface.php | 10 ++ .../View/File/Content/Fetcher.php | 28 ++++ .../View/File/Content/Type/Html.php | 27 ++++ .../View/File/Content/Type/Txt.php | 23 ++++ .../Cli/DirectoryTree/View/File/Presenter.php | 41 ++++++ .../Cli/DirectoryTree/View/Presenter.php | 33 +++++ docker-compose.dev.yml | 20 +++ docker/dev/php-cli/conf.d/xdebug.ini | 7 + docker/dev/php-cli/php-cli.dockerfile | 123 ++++++++++++++++++ docs/hw16.md | 52 ++++++++ make/dev/analyze.mk | 2 + make/dev/composer.mk | 5 + make/dev/console.mk | 2 + make/dev/docker.mk | 8 ++ make/dev/test.mk | 2 + 55 files changed, 1639 insertions(+), 1 deletion(-) create mode 100644 .env.dev.dist create mode 100644 .gitignore create mode 100644 Makefile create mode 100755 app/bin/console create mode 100644 app/composer.json create mode 100644 app/config/services.yaml create mode 100644 app/exceptions/Assert/Assert.php create mode 100644 app/exceptions/Assert/FileStorage/Directory.php create mode 100644 app/exceptions/Assert/Regexp/RegexpMatch.php create mode 100644 app/exceptions/UnexpectedValueException.php create mode 100644 app/phpstan.neon create mode 100644 app/phpunit.xml.dist create mode 100644 app/psalm.xml create mode 100644 app/src/Application.php create mode 100644 app/src/Domain/Comparator/Comparison.php create mode 100644 app/src/Domain/Comparator/Contract/CompareInterface.php create mode 100644 app/src/Domain/Comparator/FileSize/DirectorySizeComparator.php create mode 100644 app/src/Domain/Comparator/FileSize/FileSizeComparator.php create mode 100644 app/src/Domain/DirectoryTree/Builder.php create mode 100644 app/src/Domain/DirectoryTree/Contract/BuilderInterface.php create mode 100644 app/src/Domain/DirectoryTree/Directory/Content.php create mode 100644 app/src/Domain/DirectoryTree/Directory/DirectoryFirstSorter.php create mode 100644 app/src/Domain/DirectoryTree/File/Contract/FileInterface.php create mode 100644 app/src/Domain/DirectoryTree/File/Factory.php create mode 100644 app/src/Domain/DirectoryTree/Filter/SizeFilter.php create mode 100644 app/src/Domain/DirectoryTree/Iterator.php create mode 100644 app/src/Domain/DirectoryTree/Mime/GioInfoTypeGuesser.php create mode 100644 app/src/Domain/DirectoryTree/Node/AbstractNode.php create mode 100644 app/src/Domain/DirectoryTree/Node/Directory.php create mode 100644 app/src/Domain/DirectoryTree/Node/File.php create mode 100644 app/src/Domain/DirectoryTree/Size/Size.php create mode 100644 app/src/Domain/DirectoryTree/Size/Unit.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/Command/Command.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/Contract/DirectoryTreeInterface.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/Contract/PresentationInterface.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/Contract/PresentationTagInterface.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/Directory/Presenter.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/DirectoryTree.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentFetcherInterface.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentTagFetcherInterface.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Fetcher.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Html.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Txt.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/File/Presenter.php create mode 100644 app/src/Infrastructure/Cli/DirectoryTree/View/Presenter.php create mode 100644 docker-compose.dev.yml create mode 100644 docker/dev/php-cli/conf.d/xdebug.ini create mode 100644 docker/dev/php-cli/php-cli.dockerfile create mode 100644 docs/hw16.md create mode 100644 make/dev/analyze.mk create mode 100644 make/dev/composer.mk create mode 100644 make/dev/console.mk create mode 100644 make/dev/docker.mk create mode 100644 make/dev/test.mk diff --git a/.env.dev.dist b/.env.dev.dist new file mode 100644 index 000000000..c6ef85e8d --- /dev/null +++ b/.env.dev.dist @@ -0,0 +1,16 @@ +# app. +SERVER_NAME=localhost +APP_NAME=otus/architecture-patterns +APP_ENV=dev +APP_VERSION=0.0.1 +APP_DIR=/app +# php. +PHP_VERSION=8.1.5 +PHP_CLI_IMAGE=php:${PHP_VERSION}-cli-alpine +PHP_IDE_CONFIG=serverName=${SERVER_NAME} +XDEBUG_MODE=develop,debug,coverage +XDEBUG_TRIGGER=1 +# docker. +COMPOSE_DOCKER_CLI_BUILD=1 +DOCKER_BUILDKIT=1 +USER=1000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..97176802f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +/* +**/.* + +!.github + +!app +app/vendor +app/var +app/composer.lock +app/coverage.xml + +!docker +!make +!docs + +!Makefile +!phpcs.xml +!.gitignore +!docker-compose.*.yml +docker-compose.yml + +**.cache +**.env +!.*.dist +!*.dist +!**.md diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..27b71eadc --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +$(shell cp -n .env.dev.dist .env) + +include ./.env +export $(shell sed 's/=.*//' ./.env) + +-include ./make/${APP_ENV}/*.mk + +RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) +$(eval $(RUN_ARGS):;@:) + +env: + cp .env.${RUN_ARGS}.dist .env + +init: ## Build & run app developments containers. + @make env ${RUN_ARGS} + @make docker-compose ${RUN_ARGS} + @make docker-build + @make docker-up + @make composer-install + +down-clear: ## Down service and remove volumes. + docker-compose down --remove-orphans -v + rm -rf ./app/var/* + +.PHONY: help + +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-12s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index ba7ebff27..dc461d6d5 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# PHP_2022 \ No newline at end of file +# PHP_2022 + +#### Домашние задания: + +16. [Паттерны проектирования.](./docs/hw16.md) diff --git a/app/bin/console b/app/bin/console new file mode 100755 index 000000000..a9021cf44 --- /dev/null +++ b/app/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php +import('*.yaml'); + +$containerBuilder->compile(true); + +$containerBuilder->get(Application::class)->run(); diff --git a/app/composer.json b/app/composer.json new file mode 100644 index 000000000..fa154e43a --- /dev/null +++ b/app/composer.json @@ -0,0 +1,73 @@ +{ + "name": "atlance/architecture-patterns", + "description": "Course of «PHP Developer. Professional» from OTUS. Homework.", + "license": "MIT", + "type": "project", + "authors": [ + { + "name": "Anton Stepanov", + "email": "lanposts@gmail.com" + } + ], + "require": { + "php": "^8.1", + "symfony/config": "^6.3", + "symfony/console": "^6.3", + "symfony/dependency-injection": "^6.3", + "symfony/mime": "^6.3", + "symfony/yaml": "^6.3" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.30", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.8" + }, + "autoload": { + "psr-4": { + "App\\": "src/", + "App\\Exceptions\\": "exceptions" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "php-http/discovery": true, + "phpstan/extension-installer": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "6.3.*" + } + }, + "scripts": { + "all": [ + "@php-analyze", + "@test" + ], + "php-analyze": [ + "@phpstan", + "@psalm" + ], + "phpstan": "vendor/bin/phpstan analyse --no-progress", + "psalm": "vendor/bin/psalm", + "test": "XDEBUG_MODE=coverage vendor/bin/phpunit" + } +} diff --git a/app/config/services.yaml b/app/config/services.yaml new file mode 100644 index 000000000..b9624c7c4 --- /dev/null +++ b/app/config/services.yaml @@ -0,0 +1,38 @@ +parameters: + +services: + _defaults: + autowire: true + autoconfigure: true + + _instanceof: + Symfony\Component\Console\Command\Command: + tags: + - { name: 'console.command' } + + App\Infrastructure\Cli\DirectoryTree\View\Contract\PresentationTagInterface: + tags: [ 'app.cli.directory_tree_presenter' ] + + App\Infrastructure\Cli\DirectoryTree\View\File\Content\Contract\ContentTagFetcherInterface: + tags: [ 'app.cli.directory_tree_content' ] + + + App\: + resource: './../src' + + App\Infrastructure\Cli\DirectoryTree\View\File\Content\Contract\ContentFetcherInterface: + class: App\Infrastructure\Cli\DirectoryTree\View\File\Content\Fetcher + arguments: + $handlers: !tagged_locator { tag: 'app.cli.directory_tree_content', default_index_method: 'tag' } + + App\Infrastructure\Cli\DirectoryTree\View\Contract\PresentationInterface: + class: App\Infrastructure\Cli\DirectoryTree\View\Presenter + arguments: + $presenters: !tagged_locator { tag: 'app.cli.directory_tree_presenter', default_index_method: 'tag' } + + App\Application: + public: true + arguments: + $commands: !tagged 'console.command' + $name: '%env(APP_NAME)%' + $version: '%env(APP_VERSION)%' diff --git a/app/exceptions/Assert/Assert.php b/app/exceptions/Assert/Assert.php new file mode 100644 index 000000000..5509b6bbe --- /dev/null +++ b/app/exceptions/Assert/Assert.php @@ -0,0 +1,18 @@ + + + + + src/ + + + + + + + + + src + + + + diff --git a/app/psalm.xml b/app/psalm.xml new file mode 100644 index 000000000..f8821db19 --- /dev/null +++ b/app/psalm.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/Application.php b/app/src/Application.php new file mode 100644 index 000000000..92273ae90 --- /dev/null +++ b/app/src/Application.php @@ -0,0 +1,23 @@ + $commands + */ + public function __construct(string $name, string $version, iterable $commands = []) + { + parent::__construct($name, $version); + + foreach ($commands as $command) { + $this->add($command); + } + } +} diff --git a/app/src/Domain/Comparator/Comparison.php b/app/src/Domain/Comparator/Comparison.php new file mode 100644 index 000000000..1cbb96567 --- /dev/null +++ b/app/src/Domain/Comparator/Comparison.php @@ -0,0 +1,38 @@ +'; + case GTE = '>='; + case LT = '<'; + case LTE = '<='; + public function description(): string + { + return match ($this) { + self::EQ => 'is equal to', + self::NEQ => 'is not equal to', + self::GT => 'is greater than', + self::GTE => 'is greater than or equal to', + self::LT => 'is less than', + self::LTE => 'is less than or equal to' + }; + } + + public function alias(): string + { + return match ($this) { + self::EQ => 'eq', + self::NEQ => 'neq', + self::GT => 'gt', + self::GTE => 'gte', + self::LT => 'lt', + self::LTE => 'lte' + }; + } +} diff --git a/app/src/Domain/Comparator/Contract/CompareInterface.php b/app/src/Domain/Comparator/Contract/CompareInterface.php new file mode 100644 index 000000000..c2e9cbfc6 --- /dev/null +++ b/app/src/Domain/Comparator/Contract/CompareInterface.php @@ -0,0 +1,14 @@ + + */ +final class DirectorySizeComparator implements CompareInterface +{ + public function __construct(private readonly Comparison $comparison, private readonly Size $size) + { + } + + public function compare(mixed $element): bool + { + return match ($this->comparison) { + Comparison::EQ => $element->getSize()->byte() === $this->size->byte(), + Comparison::NEQ => $element->getSize()->byte() !== $this->size->byte(), + Comparison::GT => $element->getSize()->byte() > $this->size->byte(), + Comparison::GTE => $element->getSize()->byte() >= $this->size->byte(), + Comparison::LT => $element->getSize()->byte() < $this->size->byte(), + Comparison::LTE => $element->getSize()->byte() <= $this->size->byte(), + }; + } + + public function isSupported(mixed $element): bool + { + return $element instanceof Directory; + } +} diff --git a/app/src/Domain/Comparator/FileSize/FileSizeComparator.php b/app/src/Domain/Comparator/FileSize/FileSizeComparator.php new file mode 100644 index 000000000..c1ee6bcc2 --- /dev/null +++ b/app/src/Domain/Comparator/FileSize/FileSizeComparator.php @@ -0,0 +1,37 @@ + + */ +final class FileSizeComparator implements CompareInterface +{ + public function __construct(private readonly Comparison $comparison, private readonly Size $size) + { + } + + public function compare(mixed $element): bool + { + return match ($this->comparison) { + Comparison::EQ => $element->getSize()->byte() === $this->size->byte(), + Comparison::NEQ => $element->getSize()->byte() !== $this->size->byte(), + Comparison::GT => $element->getSize()->byte() > $this->size->byte(), + Comparison::GTE => $element->getSize()->byte() >= $this->size->byte(), + Comparison::LT => $element->getSize()->byte() < $this->size->byte(), + Comparison::LTE => $element->getSize()->byte() <= $this->size->byte(), + }; + } + + public function isSupported(mixed $element): bool + { + return $element instanceof File; + } +} diff --git a/app/src/Domain/DirectoryTree/Builder.php b/app/src/Domain/DirectoryTree/Builder.php new file mode 100644 index 000000000..7fdea2433 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Builder.php @@ -0,0 +1,62 @@ + */ + private array $sizes; + + public function in(string $path): self + { + Assert::dir($path); + + $builder = clone $this; + $builder->path = $path; + + return $builder; + } + + public function withDirSize(Comparator\Comparison $comparison, Size\Size $size): self + { + $builder = clone $this; + $builder->sizes[] = new Comparator\FileSize\DirectorySizeComparator($comparison, $size); + + return $builder; + } + + public function withFileSize(Comparator\Comparison $comparison, Size\Size $size): self + { + $builder = clone $this; + $builder->sizes[] = new Comparator\FileSize\FileSizeComparator($comparison, $size); + + return $builder; + } + + /** + * @return \RecursiveIterator + */ + public function build(): \RecursiveIterator + { + if (null === $this->path) { + throw new \LogicException('You must call in() method before build'); + } + + $iterator = new Iterator(new Directory($this->path)); + if ([] !== $this->sizes) { + $iterator = new SizeFilter($iterator, ...$this->sizes); + } + + return $iterator; + } +} diff --git a/app/src/Domain/DirectoryTree/Contract/BuilderInterface.php b/app/src/Domain/DirectoryTree/Contract/BuilderInterface.php new file mode 100644 index 000000000..4572e4de6 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Contract/BuilderInterface.php @@ -0,0 +1,21 @@ + */ + public function build(): \RecursiveIterator; +} diff --git a/app/src/Domain/DirectoryTree/Directory/Content.php b/app/src/Domain/DirectoryTree/Directory/Content.php new file mode 100644 index 000000000..8a02f428d --- /dev/null +++ b/app/src/Domain/DirectoryTree/Directory/Content.php @@ -0,0 +1,23 @@ + + */ + public static function list(string $path): array + { + $list = glob(str_ends_with($path, \DIRECTORY_SEPARATOR) ? "{$path}*" : "{$path}/*"); + if (!\is_array($list)) { + return []; + } + + usort($list, [DirectoryFirstSorter::class, 'sort']); + + return $list; + } +} diff --git a/app/src/Domain/DirectoryTree/Directory/DirectoryFirstSorter.php b/app/src/Domain/DirectoryTree/Directory/DirectoryFirstSorter.php new file mode 100644 index 000000000..e30afd185 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Directory/DirectoryFirstSorter.php @@ -0,0 +1,20 @@ +typeGuesser->guessMimeType($path) ?? 'undefined'; + $extension = $this->extensionGuesser->getExtensions($type)[0] ?? 'undefined'; + + return new File($name, $path, new Size((int) filesize($path)), $depth, $type, $extension); + } + + public static function default(): self + { + return new self(GioInfoTypeGuesser::getDefault(), MimeTypes::getDefault()); + } +} diff --git a/app/src/Domain/DirectoryTree/Filter/SizeFilter.php b/app/src/Domain/DirectoryTree/Filter/SizeFilter.php new file mode 100644 index 000000000..fe4079e42 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Filter/SizeFilter.php @@ -0,0 +1,46 @@ +> + */ +final class SizeFilter extends \RecursiveFilterIterator +{ + /** @var CompareInterface[] */ + private array $comparators; + + /** + * @param \RecursiveIterator $iterator + * @param CompareInterface ...$comparators + */ + public function __construct(RecursiveIterator $iterator, CompareInterface ...$comparators) + { + $this->comparators = $comparators; + + parent::__construct($iterator); + } + + public function accept(): bool + { + /** @var AbstractNode $node */ + $node = $this->current(); + + foreach ($this->comparators as $comparator) { + if (!$comparator->isSupported($node)) { + continue; + } + if (!$comparator->compare($node)) { + return false; + } + } + + return true; + } +} diff --git a/app/src/Domain/DirectoryTree/Iterator.php b/app/src/Domain/DirectoryTree/Iterator.php new file mode 100644 index 000000000..9cef703b4 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Iterator.php @@ -0,0 +1,71 @@ + + */ +final class Iterator implements \RecursiveIterator +{ + private int $position; + + /** @var array */ + private readonly array $nodes; + + public function __construct(Directory $node) + { + $this->nodes = $node->getChildren(); + } + + /** {@inheritdoc} */ + public function rewind(): void + { + $this->position = 0; + } + + /** {@inheritdoc} */ + public function valid(): bool + { + return $this->position < \count($this->nodes); + } + + /** {@inheritdoc} */ + public function key(): int + { + return $this->position; + } + + /** {@inheritdoc} */ + public function current(): mixed + { + return $this->nodes[$this->position]; + } + + /** {@inheritdoc} */ + public function next(): void + { + ++$this->position; + } + + /** {@inheritdoc} */ + public function getChildren(): self + { + if ($this->valid() && $this->nodes[$this->position] instanceof Directory) { + return new self($this->nodes[$this->position]); + } + + throw new UnexpectedValueException(sprintf('expected %s class', Directory::class)); + } + + /** {@inheritdoc} */ + public function hasChildren(): bool + { + return $this->nodes[$this->position] instanceof Directory; + } +} diff --git a/app/src/Domain/DirectoryTree/Mime/GioInfoTypeGuesser.php b/app/src/Domain/DirectoryTree/Mime/GioInfoTypeGuesser.php new file mode 100644 index 000000000..6cae085ce --- /dev/null +++ b/app/src/Domain/DirectoryTree/Mime/GioInfoTypeGuesser.php @@ -0,0 +1,82 @@ + /dev/null | grep standard::content-type | cut -d' ' -f4" + ) { + } + + /** {@inheritdoc} */ + public function isGuesserSupported(): bool + { + if (null !== self::$supported) { + return self::$supported; + } + + if ( + '\\' === \DIRECTORY_SEPARATOR + || !\function_exists('passthru') + || !\function_exists('escapeshellarg') + ) { + return self::$supported = false; + } + + ob_start(); + passthru('command -v gio', $exitStatus); + $binPath = trim((string) ob_get_clean()); + + return self::$supported = 0 === $exitStatus && '' !== $binPath; + } + + /** {@inheritdoc} */ + public function guessMimeType(string $path): ?string + { + if (!is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path)); + } + + if (!$this->isGuesserSupported()) { + throw new LogicException(sprintf('The "%s" guesser is not supported.', self::class)); + } + + ob_start(); + + passthru(sprintf($this->cmd, escapeshellarg((str_starts_with($path, '-') ? './' : '') . $path)), $exitCode); + if ($exitCode > 0) { + ob_end_clean(); + + return null; + } + + $type = trim((string) ob_get_clean()); + + $result = preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match); + + if (false === $result || 0 === $result) { + return null; + } + + return $match[1]; + } + + public static function getDefault(): self + { + return new self(); + } +} diff --git a/app/src/Domain/DirectoryTree/Node/AbstractNode.php b/app/src/Domain/DirectoryTree/Node/AbstractNode.php new file mode 100644 index 000000000..8b11214d0 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Node/AbstractNode.php @@ -0,0 +1,38 @@ +name; + } + + public function getPath(): string + { + return $this->path; + } + + public function getSize(): Size + { + return $this->size; + } + + public function getDepth(): int + { + return $this->depth; + } +} diff --git a/app/src/Domain/DirectoryTree/Node/Directory.php b/app/src/Domain/DirectoryTree/Node/Directory.php new file mode 100644 index 000000000..fbf79f606 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Node/Directory.php @@ -0,0 +1,40 @@ + */ + private array $children = []; + + public function __construct(string $path, int $depth = 0) + { + parent::__construct(pathinfo($path, \PATHINFO_BASENAME), $path, new Size(), $depth); + + ++$depth; + foreach (Content::list($this->getPath()) as $child) { + $child = $this->addChildren($child, $depth); + $this->getSize()->add($child->getSize()); + } + } + + public function addChildren(string $path, int $depth): AbstractNode + { + $child = is_file($path) ? Factory::default()->create($path, $depth) : new self($path, $depth); + $this->children[] = $child; + + return $child; + } + + /** @return array */ + public function getChildren(): array + { + return $this->children; + } +} diff --git a/app/src/Domain/DirectoryTree/Node/File.php b/app/src/Domain/DirectoryTree/Node/File.php new file mode 100644 index 000000000..fe286b780 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Node/File.php @@ -0,0 +1,38 @@ +type = $type; + $this->extension = $extension; + } + + public function getType(): string + { + return $this->type; + } + + public function getExtension(): string + { + return $this->extension; + } +} diff --git a/app/src/Domain/DirectoryTree/Size/Size.php b/app/src/Domain/DirectoryTree/Size/Size.php new file mode 100644 index 000000000..63d86f465 --- /dev/null +++ b/app/src/Domain/DirectoryTree/Size/Size.php @@ -0,0 +1,41 @@ +value * $this->unit->value; + } + + public function add(self $other): void + { + $this->value = $this->byte() + $other->byte(); + $this->unit = Unit::B; + } + + public static function fromString(string $value): self + { + $value = mb_strtoupper($value); + Assert::match(self::PATTERN, $value); + [$value, $unit] = explode(' ', $value); + + return new self((int) $value, Unit::fromName($unit)); + } + + public function __toString(): string + { + return sprintf('%s %s', $this->value, $this->unit->name); + } +} diff --git a/app/src/Domain/DirectoryTree/Size/Unit.php b/app/src/Domain/DirectoryTree/Size/Unit.php new file mode 100644 index 000000000..af8bac14e --- /dev/null +++ b/app/src/Domain/DirectoryTree/Size/Unit.php @@ -0,0 +1,28 @@ +name) { + return $case; + } + } + + throw new UnexpectedValueException(); + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/Command/Command.php b/app/src/Infrastructure/Cli/DirectoryTree/Command/Command.php new file mode 100644 index 000000000..6eb524753 --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/Command/Command.php @@ -0,0 +1,53 @@ +setName('directory:tree') + ->setDescription(<<setDefinition( + new InputDefinition([ + new InputArgument('dir', mode: InputOption::VALUE_REQUIRED), + ]) + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->tree->output( + $this->builder->in((string) $input->getArgument('dir')) + ->withDirSize(Comparison::LTE, Size::fromString('100 KB')) + ->withFileSize(Comparison::LTE, Size::fromString('100 KB')) + ->build(), + $output + ); + + return self::SUCCESS; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/DirectoryTreeInterface.php b/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/DirectoryTreeInterface.php new file mode 100644 index 000000000..694936ffe --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/DirectoryTreeInterface.php @@ -0,0 +1,16 @@ + $iterator + */ + public function output(\RecursiveIterator $iterator, OutputInterface $output): void; +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/PresentationInterface.php b/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/PresentationInterface.php new file mode 100644 index 000000000..be8910736 --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/Contract/PresentationInterface.php @@ -0,0 +1,18 @@ + + */ +interface PresentationTagInterface extends PresentationInterface +{ + public static function tag(): string; +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/Directory/Presenter.php b/app/src/Infrastructure/Cli/DirectoryTree/View/Directory/Presenter.php new file mode 100644 index 000000000..6f17b2823 --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/Directory/Presenter.php @@ -0,0 +1,28 @@ + + */ +final class Presenter implements Contract\PresentationTagInterface +{ + private const PATTERN = '%s%s [%s]'; + + public function present(mixed $element): string + { + $prefix = str_repeat('░░', $element->getDepth() - 1); + + return sprintf(self::PATTERN, $prefix, $element->getName(), (string) $element->getSize()); + } + + public static function tag(): string + { + return Directory::class; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/DirectoryTree.php b/app/src/Infrastructure/Cli/DirectoryTree/View/DirectoryTree.php new file mode 100644 index 000000000..4306840d9 --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/DirectoryTree.php @@ -0,0 +1,28 @@ + $nodes */ + $nodes = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); + + foreach ($nodes as $node) { + $output->writeln($this->presenter->present($node)); + } + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentFetcherInterface.php b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentFetcherInterface.php new file mode 100644 index 000000000..b109edf3c --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentFetcherInterface.php @@ -0,0 +1,15 @@ +|null $length + */ + public function fetch(FileInterface $file, int $length = null): ?string; +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentTagFetcherInterface.php b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentTagFetcherInterface.php new file mode 100644 index 000000000..42cdc98ac --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Contract/ContentTagFetcherInterface.php @@ -0,0 +1,10 @@ + $handlers + */ + public function __construct(private readonly ServiceProviderInterface $handlers) + { + } + + public function fetch(FileInterface $file, int $length = null): ?string + { + if ($this->handlers->has($file->getExtension())) { + return $this->handlers->get($file->getExtension())->fetch($file, $length); + } + + return null; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Html.php b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Html.php new file mode 100644 index 000000000..5e158866b --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Html.php @@ -0,0 +1,27 @@ +getPath())) { + return null; + } + + return mb_substr(trim(strip_tags($content), " \t\n\r\0\x0B\xC2\xA0"), 0, $length); + } + + public static function tag(): string + { + return self::EXT; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Txt.php b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Txt.php new file mode 100644 index 000000000..28b93c72b --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Content/Type/Txt.php @@ -0,0 +1,23 @@ +getPath(), length: $length)) ? $content : null; + } + + public static function tag(): string + { + return self::EXT; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/File/Presenter.php b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Presenter.php new file mode 100644 index 000000000..7182d9d8c --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/File/Presenter.php @@ -0,0 +1,41 @@ + + */ +final class Presenter implements PresentationTagInterface +{ + private const PATTERN = '%s%s [%s]'; + + public function __construct( + private readonly ContentFetcherInterface $fetcher, + /** @var int<0,max> */ + private readonly int $length = 50 + ) { + } + + public function present(mixed $element): string + { + $prefix = str_repeat('░░', $element->getDepth() - 1); + $presentation = sprintf(self::PATTERN, $prefix, $element->getName(), (string) $element->getSize()); + + if (null !== $content = $this->fetcher->fetch($element, $this->length)) { + $presentation = sprintf('%s < %s', $presentation, $content); + } + + return $presentation; + } + + public static function tag(): string + { + return File::class; + } +} diff --git a/app/src/Infrastructure/Cli/DirectoryTree/View/Presenter.php b/app/src/Infrastructure/Cli/DirectoryTree/View/Presenter.php new file mode 100644 index 000000000..2909f22aa --- /dev/null +++ b/app/src/Infrastructure/Cli/DirectoryTree/View/Presenter.php @@ -0,0 +1,33 @@ + + */ +final class Presenter implements PresentationInterface +{ + /** + * @param ServiceProviderInterface $presenters + */ + public function __construct(private readonly ServiceProviderInterface $presenters) + { + } + + public function present(mixed $element): string + { + if ($this->presenters->has($element::class)) { + return $this->presenters->get($element::class)->present($element); + } + + throw new UnexpectedValueException(sprintf('Presenter for class: "%s" - not found.', $element::class)); + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..5d0536e24 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,20 @@ +version: "3.9" + +services: + php-cli: + build: + context: ./docker/${APP_ENV} + dockerfile: php-cli/php-cli.dockerfile + args: + php_cli_image: ${PHP_CLI_IMAGE} + app_dir: ${APP_DIR} + user: ${USER} + env_file: [ .env ] + networks: [ backend ] + dns: [ 8.8.4.4, 8.8.8.8 ] + extra_hosts: [ "host.docker.internal:host-gateway" ] + volumes: + - ./app:${APP_DIR}:rw + +networks: + backend: diff --git a/docker/dev/php-cli/conf.d/xdebug.ini b/docker/dev/php-cli/conf.d/xdebug.ini new file mode 100644 index 000000000..3dd618843 --- /dev/null +++ b/docker/dev/php-cli/conf.d/xdebug.ini @@ -0,0 +1,7 @@ +xdebug.mode=${XDEBUG_MODE} +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.idekey=PHPSTORM +xdebug.start_with_request=yes +xdebug.log_level=3 +xdebug.log="/tmp/xdebug.log" diff --git a/docker/dev/php-cli/php-cli.dockerfile b/docker/dev/php-cli/php-cli.dockerfile new file mode 100644 index 000000000..721eea3b1 --- /dev/null +++ b/docker/dev/php-cli/php-cli.dockerfile @@ -0,0 +1,123 @@ +# syntax=docker/dockerfile:experimental +ARG php_cli_image +FROM $php_cli_image AS php-common + +ENV PHP_EXT_DIR /usr/local/lib/php/extensions/no-debug-non-zts-20210902 +RUN set -ex \ + && if [ `pear config-get ext_dir` != ${PHP_EXT_DIR} ]; then echo PHP_EXT_DIR must be `pear config-get ext_dir` && exit 1; fi + +FROM php-common AS php-build +RUN --mount=type=cache,target=/var/cache/apk set -ex \ + && apk add --update-cache $PHPIZE_DEPS + +FROM php-build AS php-ext-intl +RUN --mount=type=cache,target=/var/cache/apk set -ex \ + && apk add \ + icu-dev \ + && docker-php-ext-install intl + +FROM php-build AS php-ext-bcmath +RUN --mount=type=cache,target=/var/cache/apk set -ex \ + && docker-php-ext-install bcmath + +FROM php-build AS php-ext-pdo +RUN --mount=type=cache,target=/var/cache/apk set -ex \ + && apk add \ + postgresql-dev \ + && docker-php-ext-install pdo_pgsql + +FROM php-build AS php-ext-xdebug +RUN set -ex \ + && apk add --update linux-headers \ + && pecl install xdebug + +FROM php-build AS php-ext-memcached +RUN --mount=type=cache,target=/var/cache/apk set -ex \ + && apk add \ + libzip-dev \ + libmemcached-dev \ + && pecl install memcached + +FROM php-build AS php-ext-pcntl +RUN set -ex \ + && docker-php-ext-install pcntl + +FROM php-build AS php-ext-sockets +RUN set -ex \ + && docker-php-ext-install sockets + +FROM php-build AS php-ext-amqp +RUN set -ex \ + && apk add \ + rabbitmq-c-dev \ + && pecl install amqp-1.11.0beta + +FROM php-build AS php-ext-gd +RUN --mount=type=cache,target=/var/cache/apk \ + set -ex \ + && apk add \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install gd + +FROM php-build AS php-ext-zip +RUN --mount=type=cache,target=/var/cache/apk \ + set -ex \ + && apk add \ + libzip-dev \ + && docker-php-ext-install zip + +FROM php-build AS php-ext-redis +RUN --mount=type=cache,target=/var/cache/apk \ + set -ex \ + && pecl install redis + +FROM php-common AS php-base +COPY --from=php-ext-pdo ${PHP_EXT_DIR}/pdo_pgsql.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-intl ${PHP_EXT_DIR}/intl.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-intl /usr/local /usr/local +COPY --from=php-ext-pcntl ${PHP_EXT_DIR}/pcntl.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-bcmath ${PHP_EXT_DIR}/bcmath.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-memcached ${PHP_EXT_DIR}/memcached.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-xdebug ${PHP_EXT_DIR}/xdebug.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-sockets ${PHP_EXT_DIR}/sockets.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-amqp ${PHP_EXT_DIR}/amqp.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-gd ${PHP_EXT_DIR}/gd.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-zip ${PHP_EXT_DIR}/zip.so ${PHP_EXT_DIR}/ +COPY --from=php-ext-redis ${PHP_EXT_DIR}/redis.so ${PHP_EXT_DIR}/ +RUN --mount=type=cache,target=/var/cache/apk \ + set -ex \ + && apk add \ + libpq \ + icu \ + libpng \ + libjpeg-turbo \ + freetype \ + libzip \ + libmemcached \ + shadow \ + gettext \ + glib \ + shared-mime-info \ + xdg-utils \ + && update-mime-database /usr/share/mime \ + && docker-php-ext-enable pdo_pgsql intl pcntl bcmath memcached xdebug gd zip redis \ + && mv $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini + +COPY ./php-cli/conf.d /usr/local/etc/php/conf.d + +ARG user +RUN addgroup $user \ + && adduser -DS -h /home/$user -u 1000 -G $user $user \ + && adduser www-data $user \ + && mkdir -p /home/$user/.composer \ + && chown -R $user:$user /home/$user + +COPY --chown=$user:$user --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +USER $user:$user + +ARG app_dir +WORKDIR $app_dir diff --git a/docs/hw16.md b/docs/hw16.md new file mode 100644 index 000000000..0dd575b05 --- /dev/null +++ b/docs/hw16.md @@ -0,0 +1,52 @@ +### Паттерны проектирования. +**Цель**: +- Набор задач на реализацию изученных паттернов. +- Требуется решить минимум 5 задач. + +#### Описание/Пошаговая инструкция выполнения задания: +Выберите **пять** из **12** паттернов: +- Абстрактная фабрика +- Адаптер +- Декоратор +- Шаблонный метод +- Фабричный метод +- Итератор +- Строитель +- Наблюдатель +- Прокси +- Цепочка обязанностей +- Стратегия +- Компоновщик +- Запросите задачи у преподавателя +- Реализуйте паттерн на базе предложенного кода. + +#### Предложенный код: +Разработайте консольное приложение (`CLI`), которое будет рекурсивно выводить список каталогов и файлов начиная с +заданного каталога. + +Требования: + +- каталоги и файлы отображаем в виде дерева +- по каждому файлу и каталогу выводим, сколько он занимает +- если файл или каталог занимает больше `100 Kb`, его не выводим +- для текстовых файлов (формат `.txt`) дополнительно выводим первые `50` символов содержимого +- для `HTML`-файлов (формат `.html`) выводим первые `50` символов текста (удаляя теги) + +Использование паттернов: + +- `Компоновщик`: рекурсивный вывод каталогов и файлов +- `Цепочка обязанностей`: решение выводить/не выводить запись +- `Стратегия`: вывод информации в зависимости от типа файла + +#### Критерии оценки: +- Каждый паттерн - 2 балла +- Реализация должна соответствовать определению паттерна, `DRY`, `KISS`, `SOLID`. + +--- +### Результат. + +#### Запуск: основные команды +1. `make init dev` - сообирается `php-cli` контейнер. +3. `make directory-tree /app/src` - пример запуска комады по рекурсивному выводу дерева каталога. + +[![asciicast](https://asciinema.org/a/E9lCcQfi8xhgOc26hirhUjggC.svg)](https://asciinema.org/a/E9lCcQfi8xhgOc26hirhUjggC) \ No newline at end of file diff --git a/make/dev/analyze.mk b/make/dev/analyze.mk new file mode 100644 index 000000000..695dd1be4 --- /dev/null +++ b/make/dev/analyze.mk @@ -0,0 +1,2 @@ +php-analyze: ## Run static analyze - phpcs, phplint, phpstan, psalm. + docker-compose run --rm php-cli composer php-analyze diff --git a/make/dev/composer.mk b/make/dev/composer.mk new file mode 100644 index 000000000..0febfd804 --- /dev/null +++ b/make/dev/composer.mk @@ -0,0 +1,5 @@ +composer-install: ## Install composer dependecies. + docker-compose run --rm php-cli composer install --no-interaction --no-progress + +composer-require: + docker-compose run --rm php-cli composer req ${RUN_ARGS} diff --git a/make/dev/console.mk b/make/dev/console.mk new file mode 100644 index 000000000..5d80b1f6c --- /dev/null +++ b/make/dev/console.mk @@ -0,0 +1,2 @@ +directory-tree: ## Run console command directory tree + docker-compose run --rm php-cli bin/console directory:tree ${RUN_ARGS} diff --git a/make/dev/docker.mk b/make/dev/docker.mk new file mode 100644 index 000000000..6d4c30420 --- /dev/null +++ b/make/dev/docker.mk @@ -0,0 +1,8 @@ +docker-compose: + envsubst < docker-compose.dev.yml > docker-compose.yml + +docker-build: ## Buid dev images + docker-compose build + +docker-up: ## Start service. + docker-compose up -d diff --git a/make/dev/test.mk b/make/dev/test.mk new file mode 100644 index 000000000..66b2c2c7d --- /dev/null +++ b/make/dev/test.mk @@ -0,0 +1,2 @@ +test: ## Run phpunit tests. + docker-compose run --rm php-cli composer paratest