diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..6035eb3 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +service_name: travis-ci +coverage_clover: build/clover.xml +json_path: build/coveralls-upload.json \ No newline at end of file diff --git a/.docker/php7.3-dev/Dockerfile b/.docker/php7.3-dev/Dockerfile new file mode 100644 index 0000000..fcc639c --- /dev/null +++ b/.docker/php7.3-dev/Dockerfile @@ -0,0 +1,18 @@ +FROM php:7.3-cli + +RUN apt-get update && apt-get install -y git unzip + +ENV COMPOSER_ALLOW_SUPERUSER 1 +ENV COMPOSER_MEMORY_LIMIT -1 + +RUN mkdir /.composer_cache +ENV COMPOSER_CACHE_DIR /.composer_cache + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +RUN composer -vvv global require hirak/prestissimo + +# php extensions + +RUN pecl install xdebug +RUN docker-php-ext-enable xdebug diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f5939b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# 4 space indentation +[*.php] +indent_style = space +indent_size = 4 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +# Matches the exact files either package.json or .travis.yml +[{*.yml, *.yaml}] +indent_style = space +indent_size = 2 + +[composer.json] +indent_size = 4 diff --git a/.env b/.env new file mode 100644 index 0000000..693bbb3 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# This file is a "template" of which env vars need to be defined for your application +# Copy this file to .env file for development, create environment variables when deploying to production +# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=secret +###< symfony/framework-bundle ### + +###> common variables ### +MICROBASE_COMPOSE_PROJECT_NAME=micro-base +CI_COMMIT_REF_SLUG=master +###< common variables ### diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a760c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +vendor/ + +###> PhpStorm project profile ### +.idea/ +###< PhpStorm project profile ### + +###> phpunit/phpunit ### +phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> friendsofphp/php-cs-fixer ### +.php_cs.cache +###< friendsofphp/php-cs-fixer ### + +###> squizlabs/php_codesniffer ### +.phpcs-cache +###< squizlabs/php_codesniffer ### + +###> sensiolabs-de/deptrac ### +.deptrac.cache +###< sensiolabs-de/deptrac ### + +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..270a07b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ + +language: php + +matrix: + include: + - php: 7.3 + fast_finish: true + +env: + global: + TEST_CONFIG="phpunit.xml.dist" + +before_install: + - echo "memory_limit=2G" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini || return 0 + +install: + - travis_retry composer self-update + - composer install + +script: + - vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests + - vendor/bin/psalm --config=psalm.xml + - vendor/bin/ecs check src tests + - vendor/bin/phpmd src/ text phpmd.xml + - vendor/bin/phpunit --configuration $TEST_CONFIG + - composer validate --no-check-publish + - git log $(git describe --abbrev=0 --tags)...HEAD --no-merges --pretty=format:"* [%h](http://github.com/${TRAVIS_REPO_SLUG}/commit/%H) %s (%cN)" + +after_success: + - travis_retry php ./vendor/bin/php-coveralls -v --config .coveralls.yml -v; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..04a87a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,82 @@ +version = $(shell git describe --tags --dirty --always) +build_name = application-$(version) +# use the rest as arguments for "run" +RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) +# ...and turn them into do-nothing targets +#$(eval $(RUN_ARGS):;@:) + +.PHONY: build +build: ## build environment and initialize composer and project dependencies + docker-compose build + make composer-install + +.PHONY: stop +stop: + docker-compose stop + +.PHONY: composer-install +composer-install: ## Install project dependencies + docker-compose run --rm --no-deps php sh -lc 'composer install' + +.PHONY: composer-update +composer-update: ## Update project dependencies + docker-compose run --rm --no-deps php sh -lc 'composer update' + +.PHONY: composer-outdated +composer-outdated: ## Show outdated project dependencies + docker-compose run --rm --no-deps php sh -lc 'composer outdated' + +.PHONY: composer-validate +composer-validate: ## Validate composer config + docker-compose run --rm --no-deps php sh -lc 'composer validate --no-check-publish' + +.PHONY: composer +composer: ## Execute composer command + docker-compose run --rm --no-deps php sh -lc "composer $(RUN_ARGS)" + +.PHONY: phpunit +phpunit: ## execute project unit tests + docker-compose run --rm --no-deps php sh -lc "./vendor/bin/phpunit $(conf)" + +.PHONY: style +style: ## executes php analizers + docker-compose run --rm --no-deps php sh -lc './vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests' + docker-compose run --rm --no-deps php sh -lc './vendor/bin/psalm --config=psalm.xml' + +.PHONY: lint +lint: ## checks syntax of PHP files + docker-compose run --rm --no-deps php sh -lc './vendor/bin/parallel-lint ./ --exclude vendor --exclude bin/.phpunit' + +.PHONY: layer +layer: ## Check issues with layers (deptrac tool) + docker-compose run --rm --no-deps php sh -lc './vendor/bin/deptrac analyze --formatter-graphviz=0' + +.PHONY: logs +logs: ## look for service logs + docker-compose logs -f $(RUN_ARGS) + +.PHONY: help +help: ## Display this help message + @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: php-shell +php-shell: ## PHP shell + docker-compose run --rm php sh -lflay + +unit-tests: ## Run unit-tests suite + docker-compose run --rm php sh -lc 'vendor/bin/phpunit --testsuite unit-tests' + +static-analysis: style layer coding-standards ## Run phpstan, easycoding standarts code static analysis + +coding-standards: ## Run check and validate code standards tests + docker-compose run --rm --no-deps php sh -lc 'vendor/bin/ecs check src tests' + docker-compose run --rm --no-deps php sh -lc 'vendor/bin/phpmd src/ text phpmd.xml' + +coding-standards-fixer: ## Run code standards fixer + docker-compose run --rm --no-deps php sh -lc 'vendor/bin/ecs check src tests --fix' + +security-tests: ## The SensioLabs Security Checker + docker-compose run --rm --no-deps php sh -lc 'vendor/bin/security-checker security:check --end-point=http://security.sensiolabs.org/check_lock' + +.PHONY: test lint static-analysis phpunit coding-standards composer-validate +test: build lint static-analysis phpunit coding-standards composer-validate stop ## Run all test suites diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c517d73 --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "micro-module/base", + "type": "library", + "description": "Micro module Base common library", + "license": "proprietary", + "require": { + "php": "^7.3", + "ext-json": "*", + "beberlei/assert": "^3.2", + "psr/log": "^1.1", + "ramsey/uuid": "^3.8 || ^4.0", + "monolog/monolog": "~1.22 || ~2.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^0.4", + "php-parallel-lint/php-parallel-lint": "^1.0", + "mockery/mockery": "^1.3", + "phpmd/phpmd": "^2.8", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^9.0", + "roave/security-advisories": "dev-master", + "symplify/easy-coding-standard": "^7.2", + "vimeo/psalm": "3.5.0" + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "MicroModule\\Base\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "MicroModule\\Base\\Tests\\Unit\\": "tests/unit" + } + } +} diff --git a/depfile.yml b/depfile.yml new file mode 100644 index 0000000..9cd398e --- /dev/null +++ b/depfile.yml @@ -0,0 +1,25 @@ +paths: + - ./src +exclude_files: + +layers: + - name: Domain + collectors: + - type: className + regex: .*\\Domain\\.* + - name: Application + collectors: + - type: className + regex: .*\\Application\\.* + - name: Infrastructure + collectors: + - type: className + regex: .*\\Infrastructure\\.* + +ruleset: + Domain: + Application: + - Domain + Infrastructure: + - Domain + - Application diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f29c43f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.7" + +services: + php: + container_name: ${MICROBASE_COMPOSE_PROJECT_NAME}_php + user: 1000:1000 + build: + context: .docker/php7.3-dev + volumes: + - ~/.composer/cache/:/.composer_cache/:rw + - .:/app:rw + working_dir: /app diff --git a/ecs.yml b/ecs.yml new file mode 100644 index 0000000..629fafb --- /dev/null +++ b/ecs.yml @@ -0,0 +1,17 @@ +imports: + - { resource: 'vendor/symplify/easy-coding-standard/config/clean-code.yml' } + - { resource: 'vendor/symplify/easy-coding-standard/config/symfony.yml' } + - { resource: 'vendor/symplify/easy-coding-standard/config/php71.yml' } + +parameters: + skip: + SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingParameterTypeHint: + - 'src/Infrastructure/Testing/RedisInMemory.php' + SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingReturnTypeHint: + - 'src/Infrastructure/Testing/RedisInMemory.php' + SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff.UnusedMethod: + - 'src/Common/Alerting/AlertingProcessor.php' + Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer: + - 'src/Domain/Exception/ParentExceptionTrait.php' + Symplify\CodingStandard\Fixer\Commenting\ParamReturnAndVarTagMalformsFixer: + - 'src/Domain/Exception/ParentExceptionTrait.php' diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..700df16 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,28 @@ + + + Ruleset for PHP Mess Detector that enforces coding standards + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..c1c0981 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - vendor/phpstan/phpstan-mockery/extension.neon + +parameters: + excludes_analyse: + - src/Infrastructure/Testing/RedisInMemory.php + - src/Infrastructure/Testing/RedisFactory.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d19a08f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + tests/unit + + + + + + + + + + ./src + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..39b4cb3 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Domain/Command/CommandInterface.php b/src/Domain/Command/CommandInterface.php new file mode 100644 index 0000000..15f8674 --- /dev/null +++ b/src/Domain/Command/CommandInterface.php @@ -0,0 +1,22 @@ +parentException; + } + + /** + * Return context exception array from parent exception object. + * + * @param mixed[] $context + * @param string $contextSerializeType + * + * @return mixed[] + */ + public function getParentExceptionContext(array $context = [], string $contextSerializeType = ParentExceptionInterface::CONTEXT_SELIALIZE_NONE): array + { + $exception = $this->getParentException(); + + if ($exception instanceof ParentExceptionInterface) { + $parentException = $exception->getParentException(); + $context['parentException'] = get_class($parentException); + $context['parentExceptionErrorMessage'] = sprintf('%s: "%s" at %s line %s', get_class($parentException), $parentException->getMessage(), $parentException->getFile(), $parentException->getLine()); + $context['parentExceptionContext'] = $this->serializeContext($exception->getParentExceptionContext(), $contextSerializeType); + } + + return $context; + } + + /** + * Serialize if needed context array to string. + * + * @param mixed[] $context + * @param string $contextSerializeType + * + * @return mixed + */ + protected function serializeContext(array $context = [], string $contextSerializeType = ParentExceptionInterface::CONTEXT_SELIALIZE_NONE) + { + switch ($contextSerializeType) { + case ParentExceptionInterface::CONTEXT_SELIALIZE_BASIC: + $context = serialize($context); + + break; + + case ParentExceptionInterface::CONTEXT_SELIALIZE_JSON: + $context = json_encode($context); + + break; + + case ParentExceptionInterface::CONTEXT_SELIALIZE_PRINTR: + $context = print_r($context, true); + + break; + + case ParentExceptionInterface::CONTEXT_SELIALIZE_NONE: + default: + break; + } + + return $context; + } +} diff --git a/src/Domain/Exception/SerializationException.php b/src/Domain/Exception/SerializationException.php new file mode 100644 index 0000000..2d480ae --- /dev/null +++ b/src/Domain/Exception/SerializationException.php @@ -0,0 +1,14 @@ +logger = $logger; + + return $this; + } + + /** + * Log an regular message or warning. + * + * @param string $message + * @param int $level + * @param mixed[] $context + * + * @return $this + */ + public function logMessage(string $message, int $level, array $context = []): self + { + if (!$this->logger instanceof LoggerInterface) { + return $this; + } + + switch ($level) { + case LOG_DEBUG: + $this->logger->debug($message, $context); + + break; + + case LOG_INFO: + $this->logger->info($message, $context); + + break; + + case LOG_NOTICE: + $this->logger->notice($message, $context); + + break; + + case LOG_WARNING: + $this->logger->warning($message, $context); + + break; + + default: + throw new LoggerException(sprintf("Try to log invalid message level type '%s'", $level)); + + break; + } + + return $this; + } + + /** + * Log an exception. + * + * @param Throwable $exception The \Throwable instance + * @param int $level + * @param string $message The error message to log + * + * @return $this + */ + public function logException(Throwable $exception, int $level, string $message): self + { + if (!$this->logger instanceof LoggerInterface) { + return $this; + } + + $context = ['exception' => $exception]; + $this->parentException = $exception; + $context = $this->getParentExceptionContext($context, ParentExceptionInterface::CONTEXT_SELIALIZE_PRINTR); + + switch ($level) { + case LOG_EMERG: + $this->logger->emergency($message, $context); + + break; + + case LOG_ALERT: + $this->logger->alert($message, $context); + + break; + + case LOG_CRIT: + $this->logger->critical($message, $context); + + break; + + case LOG_ERR: + $this->logger->error($message, $context); + + break; + + default: + throw new LoggerException(sprintf("Try to log invalid error level type '%s'", $level)); + + break; + } + + return $this; + } + + /** + * Define exception level from exception type. + * + * @param Throwable $exception + * + * @return int + */ + public function getExceptionLevel(Throwable $exception): int + { + switch (true) { + case $exception instanceof EmergencyException: + return LOG_EMERG; + + case $exception instanceof AlertException: + return LOG_ALERT; + + case $exception instanceof CriticalException: + return LOG_CRIT; + + case $exception instanceof ErrorException: + return LOG_ERR; + } + + return LOG_ERR; + } + + /** + * Generate exception message. + * + * @param Throwable $exception + * + * @return string + */ + public function getExceptionMessage(Throwable $exception): string + { + return sprintf($this->exceptionMessageTemplate, get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine()); + } +} diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 0000000..e69de29