diff --git a/.gitignore b/.gitignore index 457ceba84..2abedf998 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vagrant .idea application/.env +application/vendor \ No newline at end of file diff --git a/README.md b/README.md index ddd69114a..bf5431584 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,11 @@ https://otus.ru/lessons/razrabotchik-php/?utm_source=github&utm_medium=free&utm_ # Задание -Docker -1. Установить Docker себе на локальную машину -2. Описать инфраструктуру в Docker-compose, которая включает в себя -3. nginx (обрабатывает статику, пробрасывает выполнение скриптов в fpm) -4. php-fpm (соединяется с nginx через unix-сокет) -5. redis (соединяется с php по порту) -6. memcached (соединяется с php по порту) -7. БД подключать как отдельную VM (можно на базе Homestead), либо как контейнер (но тогда не забудьте про директории с данными) -8. Не забудьте про Composer +Приложение верификации email +Реализовать приложение (сервис/функцию) для верификации email. +Реализация будет в будущем встроена в более крупное решение. +Минимальный функционал - список строк, которые необходимо проверить на наличие валидных email. +Валидация по регулярным выражениям и проверке DNS mx записи, без полноценной отправки письма-подтверждения. # Установка @@ -33,7 +29,8 @@ vagrant ssh cd application && cp .env.example .env ``` -Далее, заполните все пустые строки нужными данными в файле `.env` +Далее, заполните все пустые строки нужными данными в файле `.env`. +Особенно важным тут является переменная окружения `DOMAIN_API` После чего выполните команду @@ -41,9 +38,28 @@ cd application && cp .env.example .env sudo docker compose up -d ``` -Добавьте сайт `mysite.local` в файл `hosts` +Зайдите внутрь контейнера + +```bash +docker container exec -it myapp-php-dev bash +``` + +Выполните команду + +```bash +cd console +``` + +## Запуск и работа скрипта + +Для запуска скрипта выполните следующую команду: + ```bash -127.0.0.1 mysite.local +php app.php ``` -Готово! +Данные для теста скрипт берёт из файла + +```text +/data/www/files/emails.txt +``` diff --git a/Vagrantfile b/Vagrantfile index bb54de9d8..b2cfa9fda 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -8,6 +8,9 @@ Vagrant.configure("2") do |config| # forwarding ports config.vm.network :forwarded_port, guest: 80, host: 80 + # sync current folders to vagrant. I use rsync because of problems with syncing windows and linux + config.vm.synced_folder ".", "/vagrant", type: "rsync" + # config resources for the VM config.vm.provider "virtualbox" do |v| v.memory = 4048 diff --git a/application/.env.example b/application/.env.example index 4c15d5af3..970f203f6 100644 --- a/application/.env.example +++ b/application/.env.example @@ -7,3 +7,5 @@ MYSQL_USER=db_user MYSQL_PASSWORD= # suffix for names of docker containers DOCKER_CONTAINER_SUFFIX=-dev +# Api to the domain verification service +DOMAIN_API= diff --git a/application/composer.json b/application/composer.json new file mode 100644 index 000000000..7b7a30570 --- /dev/null +++ b/application/composer.json @@ -0,0 +1,18 @@ +{ + "name": "gesparo/hw", + "type": "project", + "autoload": { + "psr-4": { + "Gesparo\\Hw\\": "src/" + } + }, + "authors": [ + { + "name": "Maksym Melnyk" + } + ], + "require": { + "ext-curl": "*", + "vlucas/phpdotenv": "^5.5" + } +} diff --git a/application/composer.lock b/application/composer.lock new file mode 100644 index 000000000..074f885f8 --- /dev/null +++ b/application/composer.lock @@ -0,0 +1,490 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0e45f8b7a66b88ba237a3fabe4475635", + "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2023-02-25T20:23:15+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-02-25T19:38:58+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.2", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.8", + "symfony/polyfill-ctype": "^1.23", + "symfony/polyfill-mbstring": "^1.23.1", + "symfony/polyfill-php80": "^1.23.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2022-10-16T01:01:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-curl": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/application/config/php/Dockerfile b/application/config/php/Dockerfile index ef84ea5e8..a2d0d0736 100644 --- a/application/config/php/Dockerfile +++ b/application/config/php/Dockerfile @@ -1,12 +1,20 @@ FROM php:fpm RUN apt-get update \ - && apt-get install -y libmemcached-dev libssl-dev zlib1g-dev \ + && apt-get install -y libmemcached-dev libssl-dev zlib1g-dev libcurl4-gnutls-dev \ && pecl install redis \ && pecl install memcached-3.2.0 \ && docker-php-ext-enable redis memcached \ && docker-php-ext-install mysqli \ && docker-php-ext-enable mysqli \ + && docker-php-ext-install curl \ + && docker-php-ext-enable curl \ && apt-get install -y git COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +WORKDIR /data/www + +COPY ./ /data/www + +RUN composer install \ No newline at end of file diff --git a/application/console/app.php b/application/console/app.php new file mode 100644 index 000000000..8def4ae1e --- /dev/null +++ b/application/console/app.php @@ -0,0 +1,9 @@ +run(); diff --git a/application/docker-compose.yaml b/application/docker-compose.yaml index 236bc121d..bb45d19c4 100644 --- a/application/docker-compose.yaml +++ b/application/docker-compose.yaml @@ -6,12 +6,12 @@ services: - "80:80" fpm: - build: config/php + build: + context: ./ + dockerfile: config/php/Dockerfile container_name: myapp-php${DOCKER_CONTAINER_SUFFIX} ports: - ":9000" - volumes: - - ./:/data/www environment: MYAPP_MYSQL_DATABASE: ${MYSQL_DATABASE} MYAPP_MYSQL_USER: ${MYSQL_USER} diff --git a/application/files/emails.txt b/application/files/emails.txt new file mode 100644 index 000000000..27d9a2a36 --- /dev/null +++ b/application/files/emails.txt @@ -0,0 +1,8 @@ +mail@mail.ru +myawesomeemail@invalidsmtpwebsite.com +googleaccount@gmail.com +googleaccount@google.com +existingwebsitewithoutsmtp@example.com +invalidemail +login@password +ivaliddomain@some.r diff --git a/application/src/App.php b/application/src/App.php new file mode 100644 index 000000000..122c8c1a2 --- /dev/null +++ b/application/src/App.php @@ -0,0 +1,25 @@ +getRootPath())->load(); + + (new EmailService( + $_ENV['DOMAIN_API'], + PathHelper::getInstance()->getFilesPath() . 'emails.txt' + ))->make(); + } catch (\Throwable $exception) { + (new ExceptionHandler())->handle($exception); + } + } +} diff --git a/application/src/Controller/EmailService.php b/application/src/Controller/EmailService.php new file mode 100644 index 000000000..6bf3a30a2 --- /dev/null +++ b/application/src/Controller/EmailService.php @@ -0,0 +1,30 @@ +apiKey = $apiKey; + $this->fileWithEmails = $fileWithEmails; + } + + public function make(): void + { + $emailChecker = new EmailChecker(new DomainChecker($this->apiKey)); + $fileParser = new FileParser($this->fileWithEmails); + $validatedEmails = (new Validator($fileParser, $emailChecker))->validate(); + + (new Response())->response($validatedEmails); + } +} diff --git a/application/src/Email/DomainChecker.php b/application/src/Email/DomainChecker.php new file mode 100644 index 000000000..3385ed133 --- /dev/null +++ b/application/src/Email/DomainChecker.php @@ -0,0 +1,87 @@ +apiKey = $apiKey; + } + + /** + * @throws EmailException + * @throws \JsonException + */ + public function check(string $domain): bool + { + $apiResponse = $this->makeRequest($domain); + + $this->checkErrorResponse($apiResponse, $domain); + + return $this->checkIsMX($apiResponse); + } + + /** + * @throws EmailException + * @throws \JsonException + */ + private function makeRequest(string $domain): array + { + $curl = curl_init(); + $params = [ + 'domain' => $domain, + ]; + + curl_setopt($curl, CURLOPT_URL, 'https://api.api-ninjas.com/v1/dnslookup' . '?' . http_build_query($params)); + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + curl_setopt( + $curl, + CURLOPT_HTTPHEADER, + [ + "X-Api-Key: $this->apiKey" + ] + ); + + $response = curl_exec($curl); + + if ($response === false) { + throw EmailException::failToGetResponseFromTheApi($domain, $curl); + } + + curl_close($curl); + + return json_decode($response, true, 512, JSON_THROW_ON_ERROR); + } + + private function checkIsMX(array $response): bool + { + foreach ($response as $record) { + if (array_key_exists('record_type', $record) && $record['record_type'] === self::MX_RECORD_TYPE) { + return true; + } + } + + return false; + } + + /** + * @throws EmailException + */ + private function checkErrorResponse(array $response, string $domain): void + { + if (array_key_exists('error', $response)) { + throw EmailException::apiRespondWithError($response['error'], $domain); + } + } +} diff --git a/application/src/Email/EmailChecker.php b/application/src/Email/EmailChecker.php new file mode 100644 index 000000000..69d456511 --- /dev/null +++ b/application/src/Email/EmailChecker.php @@ -0,0 +1,39 @@ +domainChecker = $domainChecker; + } + + /** + * @throws EmailException + * @throws \JsonException + */ + public function check(string $email): bool + { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return false; + } + + return $this->domainChecker->check($this->getDomainName($email)); + } + + private function getDomainName(string $email): string + { + if (preg_match('/@((\w|\d)+[^.]\.[a-z]+)/', strtolower($email), $matches) !== 1) { + throw new \LogicException("Cannot get domain name from the email address '$email'"); + } + + return $matches[1]; + } +} diff --git a/application/src/Email/FileParser.php b/application/src/Email/FileParser.php new file mode 100644 index 000000000..1626aa517 --- /dev/null +++ b/application/src/Email/FileParser.php @@ -0,0 +1,90 @@ +pathToFile = $pathToFile; + $this->emails = $this->getEmailsFromFile(); + } + + /** + * @throws EmailException + */ + private function getEmailsFromFile(): array + { + $fileDescriptor = $this->openFile(); + $result = []; + + try { + foreach ($this->getFileIterator($fileDescriptor) as $email) { + // protection of empty lines + if (!is_string($email) || $email === '') { + continue; + } + + if (count($result) >= self::LIMIT_OF_EMAILS) { + throw EmailException::tooMuchEmails($this->pathToFile, self::LIMIT_OF_EMAILS); + } + + $result[] = $email; + } + } finally { + fclose($fileDescriptor); + } + + return $result; + } + + /** + * @throws EmailException + */ + private function openFile() + { + if (is_dir($this->pathToFile)) { + throw EmailException::pathIsDirectory($this->pathToFile); + } + + if (!file_exists($this->pathToFile)) { + throw EmailException::fileDoesNotExist($this->pathToFile); + } + + if (!is_readable($this->pathToFile)) { + throw EmailException::fileIsNotReadable($this->pathToFile); + } + + $descriptor = fopen($this->pathToFile, 'rb'); + + if ($descriptor === false) { + throw EmailException::cannotCreateFileDescriptorForFile($this->pathToFile); + } + + return $descriptor; + } + + private function getFileIterator($fileDescriptor): \Generator + { + while (!feof($fileDescriptor)) { + yield fgets($fileDescriptor); + } + } + + public function getEmails(): array + { + return $this->emails; + } +} diff --git a/application/src/Email/Response.php b/application/src/Email/Response.php new file mode 100644 index 000000000..ad7a91bfd --- /dev/null +++ b/application/src/Email/Response.php @@ -0,0 +1,57 @@ +divideEmails($validatedEmails); + $message = ''; + + if (empty($dividedEmails['valid'])) { + $message .= 'There is no valid emails' . PHP_EOL . PHP_EOL; + } else { + $message .= 'Valid emails:' . PHP_EOL; + $message .= implode(PHP_EOL, $dividedEmails['valid']) . PHP_EOL; + } + + $message .= PHP_EOL; + + if (empty($dividedEmails['invalid'])) { + $message .= 'All emails was valid' . PHP_EOL . PHP_EOL; + } else { + $message .= 'Invalid emails:' . PHP_EOL; + $message .= implode(PHP_EOL, $dividedEmails['invalid']) . PHP_EOL; + } + + echo $message; + } + + /** + * @param ValidateResult[] $validatedEmails + * @return mixed + */ + private function divideEmails(array $validatedEmails): array + { + return array_reduce( + $validatedEmails, + static function (array $carry, ValidateResult $item) { + if ($item->getIsValid()) { + $carry['valid'][] = $item->getEmail(); + } else { + $carry['invalid'][] = $item->getEmail(); + } + + return $carry; + }, + ['valid' => [], 'invalid' => []] + ); + } +} diff --git a/application/src/Email/ValidateResult.php b/application/src/Email/ValidateResult.php new file mode 100644 index 000000000..01f0d0507 --- /dev/null +++ b/application/src/Email/ValidateResult.php @@ -0,0 +1,27 @@ +email = $email; + $this->isValid = $isValid; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getIsValid(): bool + { + return $this->isValid; + } +} diff --git a/application/src/Email/Validator.php b/application/src/Email/Validator.php new file mode 100644 index 000000000..ee4b3c7a8 --- /dev/null +++ b/application/src/Email/Validator.php @@ -0,0 +1,36 @@ +emails = $emails; + $this->checker = $checker; + } + + /** + * @return ValidateResult[] + * @throws EmailException + * @throws \JsonException + */ + public function validate(): array + { + $result = []; + + foreach ($this->emails->getEmails() as $email) { + $email = trim($email); + $result[] = new ValidateResult($email, $this->checker->check($email)); + } + + return $result; + } +} diff --git a/application/src/Exception/EmailException.php b/application/src/Exception/EmailException.php new file mode 100644 index 000000000..1f24e0843 --- /dev/null +++ b/application/src/Exception/EmailException.php @@ -0,0 +1,46 @@ +prepareMessage($exception) . PHP_EOL); + } + + /** + * @throws \JsonException + */ + private function prepareMessage(\Throwable $exception): string + { + $trace = json_encode($exception->getTrace(), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); + + return <<getMessage()}' + Code: '{$exception->getCode()}' + Trace: + + $trace + EOL; + } +} diff --git a/application/src/PathHelper.php b/application/src/PathHelper.php new file mode 100644 index 000000000..3ce6d9adc --- /dev/null +++ b/application/src/PathHelper.php @@ -0,0 +1,36 @@ +root = $root; + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self($_SERVER['PWD'] . '/../'); + } + + return self::$instance; + } + + public function getRootPath(): string + { + return $this->root; + } + + public function getFilesPath(): string + { + return $this->getRootPath() . 'files/'; + } +}