diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..5cb98ab --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,53 @@ +name: PHP tests + +on: + pull_request: + branches: [ main ] + +jobs: + build: + + strategy: + matrix: + php: ['8.2', '8.3', '8.4'] + symfony: ['6.4', '7.3'] + + runs-on: ubuntu-latest + + name: On PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} + steps: + # https://github.com/marketplace/actions/checkout + - name: Checkout + uses: actions/checkout@v5 + + # https://github.com/marketplace/actions/setup-php-action + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl + ini-values: post_max_size=256M, max_execution_time=180 + tools: composer + + - name: Check PHP version + run: php -v + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Install Symfony ${{ matrix.symfony }} packages + run: | + composer update symfony/console:${{ matrix.symfony }} + composer update symfony/stopwatch:${{ matrix.symfony }} + + - name: Code lint PHP files + run: ./vendor/bin/phplint + + - name: Coding standards + run: ./vendor/bin/phpcs + + - name: PHPUnit + run: ./vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 00d7507..3ddd3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ vendor -.phpunit.result.cache package-lock.json -tests/example/apollo composer.lock -var/cache/* \ No newline at end of file +var/cache/* +.phpunit.cache +.phpunit.result.cache +.phplint.cache diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..fdff1a3 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,31 @@ + + + + + + + src + tests + + + + + + + + + + + + + + + + + + + tests/* + + + \ No newline at end of file diff --git a/.phplint.yml b/.phplint.yml new file mode 100644 index 0000000..e1fc3c2 --- /dev/null +++ b/.phplint.yml @@ -0,0 +1,6 @@ +path: ./ +jobs: 10 +extensions: + - php +exclude: + - vendor \ No newline at end of file diff --git a/README.md b/README.md index fbda556..a01b55e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Key features: ## Requirements -* PHP 7.4+ +* PHP 8.1+ * [Composer](https://getcomposer.org/) ## Installation diff --git a/composer.json b/composer.json index 83cabdf..a53837d 100644 --- a/composer.json +++ b/composer.json @@ -4,17 +4,18 @@ "type": "library", "license": "MIT", "require": { - "php": ">7.4.0", + "php": ">=8.2", "twig/twig": "^3.0", - "symfony/console": "^5.0", - "symfony/stopwatch": "^5.0", - "league/flysystem": "^2.2", + "symfony/console": "^6.4|^7.1", + "symfony/stopwatch": "^6.4|^7.1", + "league/flysystem": "^3.3", "masterminds/html5": "^2.7", - "league/commonmark": "^2.0", - "alchemy/zippy": "^1.0" + "league/commonmark": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^8", + "overtrue/phplint": "^9.0", + "phpunit/phpunit": ">=10.0", + "roave/security-advisories": "dev-latest", "squizlabs/php_codesniffer": "^3.5" }, "autoload": { diff --git a/docs/contributing.md b/docs/contributing.md index ab2f823..7ee9a86 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -50,18 +50,24 @@ configuration via the `src/Config.php` class. You can test your changes by using the example project. -Build files: +Run Composer install in the root: +```shell +composer install ``` -cd tests/example -../../bin/design-system + +If you have errors with this delete your local `composer.lock` file and try again. + +Build files: + +```shell +bin/design-system --path=tests/example ``` Serve: ``` -cd _dist/ -php -S localhost:8000 +php -S localhost:8000 -t tests/example/_dist/ ``` Test at: http://localhost:8000 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b0486e9..0155190 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,26 +1,18 @@ - - - - - - - - - - tests - - - - - - src - - - + + + + + + + + tests + + + + + src + + diff --git a/src/Build.php b/src/Build.php index d61a633..3eeeee1 100644 --- a/src/Build.php +++ b/src/Build.php @@ -1,9 +1,9 @@ output = $output; // Set default file permissions - $visibility = PortableVisibilityConverter::fromArray([ + $visibility = PortableVisibilityConverter::fromArray( + [ 'file' => [ 'public' => 0644, 'private' => 0600, @@ -52,8 +56,9 @@ public function __construct(Config $config, SymfonyStyle $output) 'public' => 0755, 'private' => 0700, ], - ], - Visibility::PUBLIC); + ], + Visibility::PUBLIC + ); $adapter = new LocalFilesystemAdapter($config->getRootPath(), $visibility); $this->filesystem = new Filesystem($adapter); $this->markdown = new Markdown(); @@ -110,7 +115,6 @@ public function cleanDestination(): void try { $this->filesystem->deleteDirectory($destination); $this->filesystem->createDirectory($destination); - } catch (FilesystemException | UnableToDeleteDirectory $exception) { throw new BuildException(sprintf('Cannot clean destination folder, error: %s', $exception->getMessage())); } @@ -130,13 +134,13 @@ public function buildAssets(bool $passthru = false) } // Change dir, then run command - $command = sprintf('cd %s && %s',$this->config->getRootPath(), $command); + $command = sprintf('cd %s && %s', $this->config->getRootPath(), $command); $output = ''; if ($passthru) { - passthru($command,$status); + passthru($command, $status); } else { - exec($command,$output,$status); + exec($command, $output, $status); } if ($status !== 0) { @@ -245,7 +249,7 @@ public function buildDocs() // Sort layouts in each sub-directory foreach ($pages as $subDirectory => $children) { - uasort($pages[$subDirectory], function($a, $b) { + uasort($pages[$subDirectory], function ($a, $b) { // Stick index layouts to top if ($a['filename'] === 'index') { return -1; @@ -383,7 +387,7 @@ public function buildDocsPage(string $title, string $sourcePath, string $destina /** * Create ZIP file of website assets for developer use * - * @see https://github.com/alchemy-fr/Zippy + * @see https://www.php.net/manual/en/class.ziparchive.php */ public function buildZipFile() { @@ -391,11 +395,9 @@ public function buildZipFile() $this->output->text('Skipping, no ZIP folder defined in config'); return false; } - - // Path to folder to add to ZIP archive (relative to project root) $zipFolder = $this->config->get('zip_folder'); if (empty($zipFolder)) { - $this->output->text('Skipping, no ZIP folder defined in config'); + $this->output->text('Skipping, no source folder to creat a ZIP defined in config'); return false; } $source = $this->config->getFullPath($zipFolder); @@ -413,21 +415,29 @@ public function buildZipFile() } $destination = $this->config->getFullPath($this->config->buildPath(Config::ASSETS_PATH, $zipName)) . '.zip'; - try { - $zippy = Zippy::load(); - $archive = $zippy->create($destination, [ - $zipName => $source - ], true); - - if ($this->output->isVerbose()) { - $this->output->text('* ' . $destination); - } + // Setup ZIP archive + $zip = new ZipArchive(); + if ($zip->open($destination, ZipArchive::CREATE) !== true) { + throw new BuildException(sprintf('Cannot create ZIP archive at %s', $destination)); + } - return true; + // ZIP folders + $info = pathinfo($source); + $zipFolderRegex = '/^' . preg_quote($info["dirname"] . '/' . $info["basename"], '/') . '/'; + + // Add all files in source folder to ZIP + $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_SELF; + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source, $flags)); + /** @var RecursiveDirectoryIterator $file */ + foreach ($files as $file) { + $filepath = $file->getPathname(); + $zipPath = preg_replace($zipFolderRegex, '', $filepath); + $zip->addFile($filepath, $zipPath); + } - } catch (\Alchemy\Zippy\Exception\ExceptionInterface $exception) { - throw new BuildException(sprintf('Cannot build ZIP archive for folder %s, destination %s, error: %s', $zipFolder, $destination, $exception->getMessage())); + if (!$zip->close()) { + throw new BuildException(sprintf('Cannot save ZIP archive at %s', $destination)); } + return true; } - } diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 0cb7864..449aa7e 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -1,4 +1,5 @@ setDescription('Build design system website') @@ -43,16 +45,16 @@ protected function configure() 'actions', 'a', InputOption::VALUE_REQUIRED, - 'Which actions to run ("c" = clean, "a" = assets, "p" = layouts, "t" = templates)', + 'Which actions to run ("c" = clean, "a" = assets, "d" = docs, "z" = zip)', 'cadz' ) ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $stopwatch = new Stopwatch(); - $stopwatch->start(self::$defaultName); + $stopwatch->start($this->getName()); $io = new SymfonyStyle($input, $output); $io->title(Version::NAME . ': ' . $this->getDescription()); @@ -98,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } // Finish up - $event = $stopwatch->stop(self::$defaultName); + $event = $stopwatch->stop($this->getName()); $io->newLine(); $io->text(sprintf('Execution time: %01.2f secs', $event->getDuration() / 1000)); $io->text(sprintf('Memory usage: %01.2f MB', $event->getMemory() / 1024 / 1024)); @@ -125,5 +127,4 @@ private function doZip(): bool { return strpos($this->actions, 'z') !== false; } - -} \ No newline at end of file +} diff --git a/src/Command/InitCommand.php b/src/Command/InitCommand.php index b600c96..8fd0964 100644 --- a/src/Command/InitCommand.php +++ b/src/Command/InitCommand.php @@ -1,4 +1,5 @@ setDescription('Initialise design system project') @@ -34,7 +36,7 @@ protected function configure() ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $io->title(Version::NAME . ': ' . $this->getDescription()); @@ -66,4 +68,4 @@ protected function execute(InputInterface $input, OutputInterface $output) // Finish up return self::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Command/WatchCommand.php b/src/Command/WatchCommand.php deleted file mode 100644 index 94cc0eb..0000000 --- a/src/Command/WatchCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -setDescription('Build & watch project files') - ->setHelp('This command builds the project files and watches for any changes, on chance it rebuilds files') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $output); - $io->title('Apollo: Build & Watch your project files'); - - return 0; - } -} \ No newline at end of file diff --git a/src/Config.php b/src/Config.php index cb0dc96..2a5e1b7 100644 --- a/src/Config.php +++ b/src/Config.php @@ -48,7 +48,7 @@ class Config * @throws PathDoesNotExistException * @throws \League\Flysystem\FilesystemException */ - public function __construct(string $rootPath, string $configPath = null) + public function __construct(string $rootPath, ?string $configPath = null) { $this->setRootPath($rootPath); $adapter = new LocalFilesystemAdapter($rootPath); @@ -65,7 +65,7 @@ public function __construct(string $rootPath, string $configPath = null) * @param ?string $currentUrl * @return array */ - public function getNavigation(string $currentUrl = null): array + public function getNavigation(?string $currentUrl = null): array { $navigation = []; foreach ($this->get('navigation') as $label => $url) { @@ -130,7 +130,7 @@ public function loadConfig(string $configPath) } // Require config file, which must contain a $config array - require $configPath; + require $this->getFullPath($configPath); if (!isset($config) || !is_array($config)) { throw new ConfigException(sprintf('Config file %s must contain the $config variable and it must be an array', $configPath)); } @@ -263,5 +263,4 @@ public function getPageTitle(string $title): string } return $title; } - -} \ No newline at end of file +} diff --git a/src/Exception/AssetsException.php b/src/Exception/AssetsException.php index ef0e8e0..5fe7f39 100644 --- a/src/Exception/AssetsException.php +++ b/src/Exception/AssetsException.php @@ -1,9 +1,9 @@ currentHtmlMatch, $this->currentFile)); } } - -} \ No newline at end of file +} diff --git a/src/Parser/ExampleParser.php b/src/Parser/ExampleParser.php index 959f828..08d1cce 100644 --- a/src/Parser/ExampleParser.php +++ b/src/Parser/ExampleParser.php @@ -1,4 +1,5 @@ output->isVerbose()) { $this->output->text('* ' . $destination); } - } catch (FilesystemException | UnableToWriteFile $exception) { throw new ExampleTagException(sprintf('Cannot save example template to %s (%s). Error with tag %s in doc file %s', $filename, $exception->getMessage(), $this->currentHtmlMatch, $this->currentFile)); } @@ -126,5 +126,4 @@ public function render(array $params): string ]; return $this->twig->render('@DesignSystem/partials/_example.html.twig', $data); } - -} \ No newline at end of file +} diff --git a/src/Parser/Markdown.php b/src/Parser/Markdown.php index a996229..c3448ee 100644 --- a/src/Parser/Markdown.php +++ b/src/Parser/Markdown.php @@ -62,7 +62,6 @@ public function linkProcessor(DocumentParsedEvent $event) /** @var Link $node */ foreach ($matchingNodes as $node) { - // Only update if a local URL $info = parse_url($node->getUrl()); if (count($info) > 1 && isset($info['host'])) { @@ -97,5 +96,4 @@ public function render(string $content): string { return $this->getConvertor()->convert($content); } - -} \ No newline at end of file +} diff --git a/src/Parser/ParserAbstract.php b/src/Parser/ParserAbstract.php index a4310ea..e4f14a7 100644 --- a/src/Parser/ParserAbstract.php +++ b/src/Parser/ParserAbstract.php @@ -1,4 +1,5 @@ assertTrue($resultCode === 0, $result); unlink($testConfigPath); } - -} \ No newline at end of file +} diff --git a/tests/MarkdownTest.php b/tests/MarkdownTest.php index 0b77c69..be5040d 100644 --- a/tests/MarkdownTest.php +++ b/tests/MarkdownTest.php @@ -7,7 +7,6 @@ class MarkdownTest extends TestCase { - public function testMarkdown() { $markdown = new Markdown(); @@ -36,5 +35,4 @@ public function testMarkdown() $this->assertStringNotContainsString('

Hello testing

', $html, 'Auto-link headings'); $this->assertStringContainsString('

Sub-heading

', $html, 'Auto-link headings'); } - } diff --git a/tests/ParseSpecialFunctionsTest.php b/tests/ParseSpecialFunctionsTest.php index 2abd650..dd6919d 100644 --- a/tests/ParseSpecialFunctionsTest.php +++ b/tests/ParseSpecialFunctionsTest.php @@ -8,7 +8,6 @@ final class ParseSpecialFunctionsTest extends TestCase { - public function testMatchAll() { $parser = new TagParser(); @@ -46,6 +45,4 @@ public function testInvalidHtmlTag() $this->expectException(HtmlParserException::class); $matches = $parser->matchAll('

some text

', 'example'); } - - -} \ No newline at end of file +} diff --git a/tests/example/design-system-config.php b/tests/example/design-system-config.php index 98e89c6..ad310fa 100644 --- a/tests/example/design-system-config.php +++ b/tests/example/design-system-config.php @@ -6,6 +6,7 @@ * Overrides default config settings * @see Studio24\DesignSystem\Config::$config */ + $config = [ 'site_title' => 'Studio 24 Design System', 'navigation' => [ @@ -14,6 +15,5 @@ 'Guidelines' => '/guidelines/', 'Templates' => '/templates/', ], - 'zip_folder' => 'apollo/assets' + 'zip_folder' => '_dist/assets/design-system' ]; -