mimmi20/browscap-helper

View on GitHub
src/Command/CopyTestsCommand.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * This file is part of the browscap-helper package.
 *
 * Copyright (c) 2015-2024, Thomas Mueller <mimmi20@live.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types = 1);

namespace BrowscapHelper\Command;

use BrowscapHelper\Helper\ExistingTestsLoader;
use BrowscapHelper\Helper\ExistingTestsRemover;
use BrowscapHelper\Helper\RewriteTests;
use BrowscapHelper\Source\BrowscapSource;
use BrowscapHelper\Source\CrawlerDetectSource;
use BrowscapHelper\Source\DonatjSource;
use BrowscapHelper\Source\JsonFileSource;
use BrowscapHelper\Source\MatomoSource;
use BrowscapHelper\Source\MobileDetectSource;
use BrowscapHelper\Source\PdoSource;
use BrowscapHelper\Source\TxtCounterFileSource;
use BrowscapHelper\Source\TxtFileSource;
use BrowscapHelper\Source\Ua\UserAgent;
use BrowscapHelper\Source\WhichBrowserSource;
use BrowscapHelper\Source\WootheeSource;
use Ergebnis\Json\Normalizer\Exception\InvalidIndentSize;
use Ergebnis\Json\Normalizer\Exception\InvalidIndentStyle;
use Ergebnis\Json\Normalizer\Exception\InvalidJsonEncodeOptions;
use Ergebnis\Json\Normalizer\Exception\InvalidNewLineString;
use JsonException;
use PDO;
use PDOException;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use UnexpectedValueException;

use function array_key_exists;
use function array_map;
use function count;
use function is_string;
use function json_encode;
use function sprintf;
use function trim;

use const JSON_THROW_ON_ERROR;

final class CopyTestsCommand extends Command
{
    /** @throws LogicException */
    public function __construct(
        private readonly ExistingTestsLoader $testsLoader,
        private readonly ExistingTestsRemover $testsRemover,
        private readonly RewriteTests $rewriteTests,
        private readonly string $sourcesDirectory = '',
    ) {
        parent::__construct();
    }

    /**
     * Configures the current command.
     *
     * @throws InvalidArgumentException
     */
    protected function configure(): void
    {
        $this
            ->setName('copy-tests')
            ->setDescription('Copies tests from browscap and other libraries')
            ->addOption(
                'resources',
                null,
                InputOption::VALUE_REQUIRED,
                'Where the resource files are located',
                $this->sourcesDirectory,
            );
    }

    /**
     * Executes the current command.
     *
     * This method is not abstract because you can use this class
     * as a concrete class. In this case, instead of defining the
     * execute() method, you set the code to execute by passing
     * a Closure to the setCode() method.
     *
     * @see    setCode()
     *
     * @param InputInterface  $input  An InputInterface instance
     * @param OutputInterface $output An OutputInterface instance
     *
     * @return int 0 if everything went fine, or an error code
     *
     * @throws LogicException           When this abstract method is not implemented
     * @throws InvalidArgumentException
     * @throws InvalidJsonEncodeOptions
     * @throws InvalidNewLineString
     * @throws InvalidIndentStyle
     * @throws InvalidIndentSize
     * @throws UnexpectedValueException
     * @throws \LogicException
     * @throws RuntimeException
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $testSource = 'tests';
        $txtChecks  = [];

        $sources = [new JsonFileSource($testSource)];

        $output->writeln('reading already existing tests ...', OutputInterface::VERBOSITY_NORMAL);

        foreach ($this->testsLoader->getProperties($output, $sources) as $row) {
            $seachHeader = (string) UserAgent::fromHeaderArray($row['headers']);

            if (array_key_exists($seachHeader, $txtChecks)) {
                $output->writeln(
                    '<error>' . sprintf(
                        'Header "%s" added more than once --> skipped',
                        $seachHeader,
                    ) . '</error>',
                    OutputInterface::VERBOSITY_NORMAL,
                );

                continue;
            }

            $txtChecks[$seachHeader] = 1;
        }

        $sourcesDirectory = $input->getOption('resources');

        $this->testsRemover->remove($output, $testSource);

        $output->writeln('init sources ...', OutputInterface::VERBOSITY_NORMAL);

        $sources = [
            new BrowscapSource(),
            new CrawlerDetectSource(),
            new DonatjSource(),
            new MatomoSource(),
            new MobileDetectSource(),
            new WhichBrowserSource(),
            new WootheeSource(),
            new TxtFileSource($sourcesDirectory),
            new TxtCounterFileSource($sourcesDirectory),
        ];

        try {
            $dbname   = 'ua';
            $host     = 'localhost';
            $port     = 3306;
            $charset  = 'utf8mb4';
            $user     = 'root';
            $password = '';

            $driverOptions = [
                PDO::ATTR_EMULATE_PREPARES => false,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_PERSISTENT => true,
                PDO::MYSQL_ATTR_DIRECT_QUERY => false,
                PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
            ];

            $pdo = new PDO(
                sprintf('mysql:dbname=%s;host=%s;port=%s;charset=%s', $dbname, $host, $port, $charset),
                $user,
                $password,
                $driverOptions,
            );

            $sources[] = new PdoSource($pdo);
        } catch (PDOException) {
            $output->writeln(
                '<error>An error occured while initializing the database</error>',
                OutputInterface::VERBOSITY_NORMAL,
            );
        }

        $output->writeln('copy tests from sources ...', OutputInterface::VERBOSITY_NORMAL);
        $txtTotalCounter = 0;

        foreach ($this->testsLoader->getProperties($output, $sources) as $test) {
            $test['headers'] = array_map(
                static function (string | array $header): string {
                    if (is_string($header)) {
                        return trim($header);
                    }

                    return '';
                },
                $test['headers'],
            );

            $seachHeader = (string) UserAgent::fromHeaderArray($test['headers']);

            if (array_key_exists($seachHeader, $txtChecks)) {
                continue;
            }

            try {
                json_encode($seachHeader, JSON_THROW_ON_ERROR);
            } catch (JsonException) {
                $output->writeln(
                    '<comment>' . sprintf(
                        'Header "%s" contained illegal characters --> skipped',
                        $seachHeader,
                    ) . '</comment>',
                    OutputInterface::VERBOSITY_VERY_VERBOSE,
                );

                continue;
            }

            $txtChecks[$seachHeader] = $test;
            ++$txtTotalCounter;
        }

        $output->writeln('rewrite tests ...', OutputInterface::VERBOSITY_NORMAL);

        $this->rewriteTests->rewrite($output, $txtChecks, $testSource);

        $output->writeln('', OutputInterface::VERBOSITY_NORMAL);
        $output->writeln(
            'tests copied for Browscap helper:    ' . $txtTotalCounter,
            OutputInterface::VERBOSITY_NORMAL,
        );
        $output->writeln(
            'tests available for Browscap helper: ' . count($txtChecks),
            OutputInterface::VERBOSITY_NORMAL,
        );

        return self::SUCCESS;
    }
}