From 495e387bec8ebe68fd2510cd3b2da7763897d665 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 26 Aug 2026 18:39:53 -0600 Subject: [PATCH 1/3] Add scaffold:winter.redirect demo-data command Adds a dev-only, env-guarded, idempotent console command that seeds a varied set of redirects (every match/target type, enabled/disabled/ scheduled states, long URLs, category assignments + pagination filler) plus the client-hit and log data the statistics charts and dashboard report widgets render from. Supports --fresh. Includes a PHPUnit test covering command registration and the production guard. (The full seed is verified against a real install; this plugin's Vdlp-heritage migrations are not fully provisioned by the isolated plugin:refresh test harness, so the seed itself is not asserted here.) Co-Authored-By: Claude Opus 4.8 (1M context) --- Plugin.php | 2 + console/ScaffoldCommand.php | 480 ++++++++++++++++++++++++++++ tests/cases/ScaffoldCommandTest.php | 57 ++++ 3 files changed, 539 insertions(+) create mode 100644 console/ScaffoldCommand.php create mode 100644 tests/cases/ScaffoldCommandTest.php diff --git a/Plugin.php b/Plugin.php index 7ca6f80..30153c4 100644 --- a/Plugin.php +++ b/Plugin.php @@ -15,6 +15,7 @@ use Winter\Redirect\Classes\Observers; use Winter\Redirect\Classes\RedirectMiddleware; use Winter\Redirect\Console\PublishRedirectsCommand; +use Winter\Redirect\Console\ScaffoldCommand; use Winter\Redirect\Models; use Winter\Redirect\ReportWidgets; @@ -299,6 +300,7 @@ public function registerSchedule($schedule): void private function registerConsoleCommands(): void { $this->registerConsoleCommand('winter.redirect.publish-redirects', PublishRedirectsCommand::class); + $this->registerConsoleCommand('winter.redirect.scaffold', ScaffoldCommand::class); } private function registerCustomValidators(): void diff --git a/console/ScaffoldCommand.php b/console/ScaffoldCommand.php new file mode 100644 index 0000000..e5f31b3 --- /dev/null +++ b/console/ScaffoldCommand.php @@ -0,0 +1,480 @@ +getLaravel()->environment('production')) { + $this->error('scaffold:winter.redirect cannot run in the production environment.'); + + return self::FAILURE; + } + + if ($this->option('fresh')) { + $this->deleteExisting(); + } + + if (Redirect::where('description', 'like', self::MARKER . '%')->exists()) { + $this->warn('Winter.Redirect scaffold data already exists. Use --fresh to recreate it.'); + + return self::SUCCESS; + } + + $categories = $this->createCategories(); + $this->info('Created ' . count($categories) . ' categories.'); + + $redirects = $this->createRedirects($categories); + $this->info('Created ' . count($redirects) . ' redirects.'); + + [$clientCount, $logCount] = $this->seedHits($redirects); + $this->info("Seeded {$clientCount} client hit records and {$logCount} redirect log rows."); + + $this->newLine(); + $this->line('Redirects: ' . Backend::url('winter/redirect/redirects')); + $this->line('Categories: ' . Backend::url('winter/redirect/categories')); + $this->line('Statistics: ' . Backend::url('winter/redirect/statistics')); + $this->line('Dashboard: ' . Backend::url('backend/dashboard') . ' (add the "Top 10 redirects" + "Create redirect" report widgets)'); + + return self::SUCCESS; + } + + /** + * Remove previously scaffolded redirects (their clients + logs cascade via + * FK, but we also delete explicitly in case the DB driver does not) and the + * scaffold categories. + */ + protected function deleteExisting(): void + { + $redirects = Redirect::where('description', 'like', self::MARKER . '%')->get(); + + foreach ($redirects as $redirect) { + Client::where('redirect_id', $redirect->id)->delete(); + RedirectLog::where('redirect_id', $redirect->id)->delete(); + $redirect->delete(); + } + + $categories = Category::where('name', 'like', self::CATEGORY_PREFIX . '%')->get(); + foreach ($categories as $category) { + $category->delete(); + } + + if ($redirects->isNotEmpty() || $categories->isNotEmpty()) { + $this->info("Removed {$redirects->count()} scaffold redirect(s) and {$categories->count()} category(ies)."); + } + } + + /** + * @return array + */ + protected function createCategories(): array + { + $marketing = $this->makeCategory('Marketing campaigns'); + $legacy = $this->makeCategory('Legacy site URLs'); + $longName = $this->makeCategory( + 'A deliberately very long category name used to test truncation and wrapping in the ' + . 'categories list, the redirect form relation picker and the redirects list filter dropdown' + ); + + return compact('marketing', 'legacy', 'longName'); + } + + protected function makeCategory(string $name): Category + { + $category = new Category(); + $category->name = self::CATEGORY_PREFIX . ' ' . $name; + $category->save(); + + return $category; + } + + /** + * Build a varied set of redirects covering every match type, every target + * type, enabled/disabled/scheduled states, long URLs and category + * assignments — plus filler to paginate the 20-per-page list. + * + * @param array $cats + * @return Redirect[] + */ + protected function createRedirects(array $cats): array + { + $redirects = []; + $sort = 1; + + // 1. Exact -> path/url, permanent, enabled, categorised. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/old-home', + 'to_url' => '/', + 'status_code' => 301, + 'is_enabled' => true, + 'category_id' => $cats['legacy']->id, + 'sort_order' => $sort++, + 'note' => 'exact -> path, 301, enabled', + ]); + + // 2. Exact -> external URL, temporary, enabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/promo', + 'to_url' => 'https://example.com/landing/summer-sale', + 'status_code' => 302, + 'is_enabled' => true, + 'category_id' => $cats['marketing']->id, + 'sort_order' => $sort++, + 'note' => 'exact -> external, 302, enabled', + ]); + + // 3. Placeholders match -> path, permanent, enabled, with requirements. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_PLACEHOLDERS, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/blog/{category}/{slug}', + 'to_url' => '/articles/{slug}', + 'status_code' => 301, + 'is_enabled' => true, + 'category_id' => $cats['legacy']->id, + 'sort_order' => $sort++, + 'requirements' => [ + ['placeholder' => 'category', 'requirement' => '[a-z0-9\-]+', 'replacement' => null], + ['placeholder' => 'slug', 'requirement' => '[a-z0-9\-]+', 'replacement' => null], + ], + 'note' => 'placeholders -> path, 301, enabled, with requirements', + ]); + + // 4. Regex match -> path, permanent, disabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_REGEX, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/^\/product\/(\d+)\/.*$/', + 'to_url' => '/shop/item/$1', + 'status_code' => 301, + 'is_enabled' => false, + 'category_id' => $cats['legacy']->id, + 'sort_order' => $sort++, + 'note' => 'regex -> path, 301, DISABLED', + ]); + + // 5. Exact -> CMS page, temporary, enabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_CMS_PAGE, + 'from_url' => '/contact-us', + 'cms_page' => 'contact', + 'status_code' => 302, + 'is_enabled' => true, + 'sort_order' => $sort++, + 'note' => 'exact -> cms_page, 302, enabled', + ]); + + // 6. Exact -> static page (Winter.Pages), see-other, enabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_STATIC_PAGE, + 'from_url' => '/about-old', + 'static_page' => 'about', + 'status_code' => 303, + 'is_enabled' => true, + 'sort_order' => $sort++, + 'note' => 'exact -> static_page, 303, enabled', + ]); + + // 7. Exact, no target, 404 Not Found, enabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_NONE, + 'from_url' => '/deleted-page', + 'status_code' => 404, + 'is_enabled' => true, + 'sort_order' => $sort++, + 'note' => 'exact -> none, 404 not found, enabled', + ]); + + // 8. Exact, no target, 410 Gone, enabled. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_NONE, + 'from_url' => '/retired-offer', + 'status_code' => 410, + 'is_enabled' => true, + 'category_id' => $cats['marketing']->id, + 'sort_order' => $sort++, + 'note' => 'exact -> none, 410 gone, enabled', + ]); + + // 9. Scheduled redirect (active window in the past -> renders "special"/inactive row + warning). + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/xmas-2024', + 'to_url' => '/happy-new-year', + 'status_code' => 302, + 'is_enabled' => true, + 'category_id' => $cats['marketing']->id, + 'from_date' => Carbon::now()->subYear()->startOfYear(), + 'to_date' => Carbon::now()->subYear()->endOfYear(), + 'sort_order' => $sort++, + 'note' => 'scheduled, expired window (inactive/special row)', + ]); + + // 10. Scheduled redirect (currently active window). + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/current-campaign', + 'to_url' => '/campaigns/live', + 'status_code' => 302, + 'is_enabled' => true, + 'category_id' => $cats['marketing']->id, + 'from_date' => Carbon::now()->subMonth(), + 'to_date' => Carbon::now()->addMonth(), + 'sort_order' => $sort++, + 'note' => 'scheduled, currently active window', + ]); + + // 11. Deliberately very long from/to URLs to test list truncation + form wrapping. + $longFrom = '/legacy/deeply/nested/category/structure/that/keeps/going/' + . 'products/2019/archived/discontinued/' + . str_repeat('segment/', 6) . 'final-item-with-a-very-long-slug-name'; + $longTo = 'https://www.example.com/new/consolidated/catalogue/' + . str_repeat('path/', 8) . 'destination?utm_source=redirect&utm_medium=legacy&utm_campaign=migration'; + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_EXACT, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => $longFrom, + 'to_url' => $longTo, + 'status_code' => 301, + 'is_enabled' => true, + 'category_id' => $cats['longName']->id, + 'sort_order' => $sort++, + 'note' => 'very long from/to URLs', + ]); + + // 12. Regex -> external, disabled, uncategorised. + $redirects[] = $this->makeRedirect([ + 'match_type' => Redirect::TYPE_REGEX, + 'target_type' => Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => '/^\/news\/(\d{4})\/(\d{2})\/(.+)$/', + 'to_url' => 'https://news.example.com/$1-$2/$3', + 'status_code' => 301, + 'is_enabled' => false, + 'sort_order' => $sort++, + 'note' => 'regex -> external, DISABLED', + ]); + + // Filler rows to paginate the 20-per-page list and give the "hits" column + // a range of values. Alternate match/target types and enabled state. + // (Regex filler is skipped here because a regex match type requires the + // from_url itself to be a valid regular expression — the varied regex + // examples above already cover that match type.) + $matchTypes = [Redirect::TYPE_EXACT, Redirect::TYPE_PLACEHOLDERS]; + $statusCodes = [301, 302, 303, 404, 410]; + for ($i = 1; $i <= 22; $i++) { + $status = $statusCodes[$i % count($statusCodes)]; + $isNoTarget = in_array($status, [404, 410], true); + $isPlaceholder = ($matchTypes[$i % count($matchTypes)] === Redirect::TYPE_PLACEHOLDERS); + + $attrs = [ + 'match_type' => $matchTypes[$i % count($matchTypes)], + 'target_type' => $isNoTarget ? Redirect::TARGET_TYPE_NONE : Redirect::TARGET_TYPE_PATH_URL, + 'from_url' => $isPlaceholder ? "/legacy/{section}/path-{$i}" : "/legacy/path-{$i}", + 'status_code' => $status, + 'is_enabled' => ($i % 4 !== 0), + 'sort_order' => $sort++, + 'note' => "filler #{$i}", + ]; + + if (!$isNoTarget) { + $attrs['to_url'] = $isPlaceholder ? "/new/{section}/destination-{$i}" : "/new/destination-{$i}"; + } + if ($i % 3 === 0) { + $attrs['category_id'] = $cats['legacy']->id; + } + + $redirects[] = $this->makeRedirect($attrs); + } + + return $redirects; + } + + /** + * Persist a single redirect, stamping the scaffold marker into its + * `description` so `--fresh` can find it. `note` becomes the visible + * description suffix. + * + * @param array $attrs + */ + protected function makeRedirect(array $attrs): Redirect + { + $note = $attrs['note'] ?? ''; + unset($attrs['note']); + + $redirect = new Redirect(); + $redirect->fill($attrs); + $redirect->description = trim(self::MARKER . ' ' . $note); + $redirect->from_scheme = Redirect::SCHEME_AUTO; + $redirect->to_scheme = Redirect::SCHEME_AUTO; + $redirect->save(); + + return $redirect; + } + + /** + * Seed hit data so the statistics charts + dashboard widgets render. + * + * The statistics surfaces read `winter_redirect_clients` (one row per hit, + * carrying day/month/year/timestamp and an optional crawler string). We + * spread hits across the current and previous month so both the "hits per + * day" chart (current month) and the "hits per month" chart have data, and + * we mark a slice of them as crawler hits so the crawler dataset + the "top + * crawlers this month" chart populate. + * + * We also maintain each redirect's `hits`/`last_used_at` counter (drives the + * list column + "Top 10 redirects" widget) and write a deduped `RedirectLog` + * row (drives the per-redirect "Logs" relation tab). + * + * @param Redirect[] $redirects + * @return array{0:int,1:int} [clientCount, logCount] + */ + protected function seedHits(array $redirects): array + { + // A few realistic crawler UA fragments for the "top crawlers" chart. + $crawlerSamples = array_slice([ + 'Googlebot', 'bingbot', 'YandexBot', 'DuckDuckBot', 'facebookexternalhit', + ], 0, 5); + + // Only enabled, active redirects realistically accrue hits; weight the + // "featured" first few so the Top-redirects chart has a clear ranking. + $eligible = array_values(array_filter($redirects, static function (Redirect $r): bool { + return (bool) $r->is_enabled; + })); + + $now = Carbon::now(); + $clientRows = []; + $logAgg = []; // [redirectId] => ['hits' => n, 'log' => [...]] + $counter = 0; + + foreach ($eligible as $index => $redirect) { + // Give earlier redirects more hits (clear ranking for the top chart). + $baseHits = max(3, 60 - ($index * 4)); + + for ($h = 0; $h < $baseHits; $h++) { + $counter++; + + // Spread across ~75 days so both current + previous month have data. + $timestamp = $now->copy()->subDays(random_int(0, 74)) + ->setTime(random_int(0, 23), random_int(0, 59), random_int(0, 59)); + + // ~25% crawler hits. + $isCrawler = ($counter % 4 === 0); + $crawler = $isCrawler ? $crawlerSamples[$counter % count($crawlerSamples)] : null; + + $clientRows[] = [ + 'redirect_id' => $redirect->id, + 'timestamp' => $timestamp, + 'day' => $timestamp->day, + 'month' => $timestamp->month, + 'year' => $timestamp->year, + 'crawler' => $crawler, + ]; + + // Aggregate for the redirect hit counter + last_used_at. + if (!isset($logAgg[$redirect->id])) { + $logAgg[$redirect->id] = ['hits' => 0, 'last' => $timestamp]; + } + $logAgg[$redirect->id]['hits']++; + if ($timestamp->gt($logAgg[$redirect->id]['last'])) { + $logAgg[$redirect->id]['last'] = $timestamp; + } + } + } + + // Bulk-insert client hit records. + foreach (array_chunk($clientRows, 500) as $chunk) { + Client::insert(array_map(static function (array $row): array { + return [ + 'redirect_id' => $row['redirect_id'], + 'timestamp' => $row['timestamp']->toDateTimeString(), + 'day' => $row['day'], + 'month' => $row['month'], + 'year' => $row['year'], + 'crawler' => $row['crawler'], + ]; + }, $chunk)); + } + + // Update each redirect's hit counter + last_used_at and write a deduped log. + $logCount = 0; + foreach ($logAgg as $redirectId => $agg) { + /** @var Redirect $redirect */ + $redirect = collect($eligible)->firstWhere('id', $redirectId); + + $redirect->forceFill([ + 'hits' => $agg['hits'], + 'last_used_at' => $agg['last'], + ])->save(); + + $toUrl = $redirect->to_url ?? ($redirect->cms_page ?? ($redirect->static_page ?? '')); + + RedirectLog::create([ + 'redirect_id' => $redirectId, + 'from_to_hash' => sha1(($redirect->from_url ?? '') . '|' . $toUrl), + 'status_code' => (string) $redirect->status_code, + 'from_url' => (string) $redirect->from_url, + 'to_url' => (string) $toUrl, + 'hits' => $agg['hits'], + ]); + $logCount++; + } + + return [count($clientRows), $logCount]; + } +} diff --git a/tests/cases/ScaffoldCommandTest.php b/tests/cases/ScaffoldCommandTest.php new file mode 100644 index 0000000..5249012 --- /dev/null +++ b/tests/cases/ScaffoldCommandTest.php @@ -0,0 +1,57 @@ +app->register(\Winter\Redirect\ServiceProvider::class); + + // Plugin console commands are registered via ConsoleApplication::starting, which has + // already fired by the time the test harness boots the plugin — so the command isn't + // resolvable through Artisan here. Register it directly with the kernel for the test. + $this->app->make(ConsoleKernel::class)->registerCommand(new ScaffoldCommand()); + } + + public function testCommandIsRegistered() + { + $this->assertArrayHasKey('scaffold:winter.redirect', Artisan::all()); + } + + public function testRefusesToRunInProduction() + { + $this->app['env'] = 'production'; + + $exitCode = Artisan::call('scaffold:winter.redirect'); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('production', Artisan::output()); + $this->assertSame( + 0, + Redirect::where('description', 'like', ScaffoldCommand::MARKER . '%')->count(), + 'Nothing should be created in production.' + ); + + $this->app['env'] = 'testing'; + } +} From 1263c3ded3102a4d8feb1c8730074124527b1e1d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 26 Aug 2026 19:12:53 -0600 Subject: [PATCH 2/3] Do not seed current hits for out-of-window redirects Restrict hit-seeding to enabled redirects whose active date window (if any) currently contains now, so an expired scheduled redirect isn't given current-month hits. Addresses CodeRabbit review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) --- console/ScaffoldCommand.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/console/ScaffoldCommand.php b/console/ScaffoldCommand.php index e5f31b3..9e08211 100644 --- a/console/ScaffoldCommand.php +++ b/console/ScaffoldCommand.php @@ -391,13 +391,17 @@ protected function seedHits(array $redirects): array 'Googlebot', 'bingbot', 'YandexBot', 'DuckDuckBot', 'facebookexternalhit', ], 0, 5); - // Only enabled, active redirects realistically accrue hits; weight the - // "featured" first few so the Top-redirects chart has a clear ranking. - $eligible = array_values(array_filter($redirects, static function (Redirect $r): bool { - return (bool) $r->is_enabled; - })); - $now = Carbon::now(); + + // Only enabled redirects whose active date window (if any) currently + // contains "now" realistically accrue hits — so an expired scheduled + // redirect isn't given current-month hits. Weight the "featured" first + // few so the Top-redirects chart has a clear ranking. + $eligible = array_values(array_filter($redirects, static function (Redirect $r) use ($now): bool { + return (bool) $r->is_enabled + && ($r->from_date === null || $r->from_date->lte($now)) + && ($r->to_date === null || $r->to_date->gte($now)); + })); $clientRows = []; $logAgg = []; // [redirectId] => ['hits' => n, 'log' => [...]] $counter = 0; From ce6682dd242baa72ad0d22b80d464a77d0eb064e Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 26 Aug 2026 19:33:42 -0600 Subject: [PATCH 3/3] Add Code Quality (phpcs) workflow Bring the plugin to full CI parity with its siblings/EasyForms: add the phpcs Code Quality workflow (+ the phpcs-pr / phpcs-push diff-scoped utilities and the Winter CMS Plugins phpcs.xml ruleset). Verified locally with the phpcs-pr utility against the base branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/code-quality.yaml | 37 ++++++++++ .github/workflows/utilities/phpcs-pr | 83 ++++++++++++++++++++++ .github/workflows/utilities/phpcs-push | 83 ++++++++++++++++++++++ phpcs.xml | 97 ++++++++++++++++++++++++++ 4 files changed, 300 insertions(+) create mode 100644 .github/workflows/code-quality.yaml create mode 100755 .github/workflows/utilities/phpcs-pr create mode 100755 .github/workflows/utilities/phpcs-push create mode 100644 phpcs.xml diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml new file mode 100644 index 0000000..5f0e1e5 --- /dev/null +++ b/.github/workflows/code-quality.yaml @@ -0,0 +1,37 @@ +name: Code Quality + +on: + pull_request: + push: + branches: + - main + +jobs: + codeQuality: + runs-on: ubuntu-latest + name: PHP + steps: + - name: Cancel previous incomplete runs + uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ github.token }} + + - name: Checkout changes + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install PHP and PHP Code Sniffer + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip + tools: phpcs + + - name: Run code quality checks (on push) + if: github.event_name == 'push' + run: ./.github/workflows/utilities/phpcs-push ${{ github.sha }} + + - name: Run code quality checks (on pull request) + if: github.event_name == 'pull_request' + run: ./.github/workflows/utilities/phpcs-pr ${{ github.base_ref }} diff --git a/.github/workflows/utilities/phpcs-pr b/.github/workflows/utilities/phpcs-pr new file mode 100755 index 0000000..b997066 --- /dev/null +++ b/.github/workflows/utilities/phpcs-pr @@ -0,0 +1,83 @@ +#!/usr/bin/env php + ($line[3] === 'warning'), + 'message' => $line[4], + 'line' => $line[1], + ]; + } + + // Render report + echo "\e[0;31mFound " + . ((count($lines) === 1) + ? '1 issue' + : count($lines) . ' issues') + . " with code quality.\e[0m"; + echo "\n"; + + foreach ($files as $file => $errors) { + echo "\n"; + echo "\e[1;37m" . str_replace('"', '', $file) . "\e[0m"; + echo "\n\n"; + + foreach ($errors as $error) { + echo "\e[2m" . str_pad(' L' . $error['line'], 7) . " | \e[0m"; + if ($error['warning'] === false) { + echo "\e[0;31mERR:\e[0m "; + } else { + echo "\e[1;33mWARN:\e[0m "; + } + echo $error['message']; + echo "\n"; + } + } + exit(1); +} diff --git a/.github/workflows/utilities/phpcs-push b/.github/workflows/utilities/phpcs-push new file mode 100755 index 0000000..add55df --- /dev/null +++ b/.github/workflows/utilities/phpcs-push @@ -0,0 +1,83 @@ +#!/usr/bin/env php + ($line[3] === 'warning'), + 'message' => $line[4], + 'line' => $line[1], + ]; + } + + // Render report + echo "\e[0;31mFound " + . ((count($lines) === 1) + ? '1 issue' + : count($lines) . ' issues') + . " with code quality.\e[0m"; + echo "\n"; + + foreach ($files as $file => $errors) { + echo "\n"; + echo "\e[1;37m" . str_replace('"', '', $file) . "\e[0m"; + echo "\n\n"; + + foreach ($errors as $error) { + echo "\e[2m" . str_pad(' L' . $error['line'], 7) . " | \e[0m"; + if ($error['warning'] === false) { + echo "\e[0;31mERR:\e[0m "; + } else { + echo "\e[1;33mWARN:\e[0m "; + } + echo $error['message']; + echo "\n"; + } + } + exit(1); +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..e9aac5a --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,97 @@ + + + The coding standard for Winter CMS Plugins. + + + + + + + + + + + + + + + + + + + + */behaviors/*/partials/*\.php + */components/*/*\.php + */controllers/*/*\.php + */formwidgets/*/partials/*\.php + */reportwidgets/*/partials/*\.php + */widgets/*/partials/*\.php + */partials/*\.php + + + + + */updates/*\.php + */tests/* + + + + + */tests/* + + + + + */behaviors/*/partials/*\.php + */components/*/*\.php + */controllers/*/*\.php + */formwidgets/*/partials/*\.php + */reportwidgets/*/partials/*\.php + */widgets/*/partials/*\.php + */partials/*\.php + + + + + */behaviors/*/partials/*\.php + */components/*/*\.php + */controllers/*/*\.php + */formwidgets/*/partials/*\.php + */reportwidgets/*/partials/*\.php + */widgets/*/partials/*\.php + */partials/*\.php + + + + */behaviors/*/partials/*\.php + */components/*/*\.php + */controllers/*/*\.php + */formwidgets/*/partials/*\.php + */reportwidgets/*/partials/*\.php + */widgets/*/partials/*\.php + */partials/*\.php + + + + + + + . + */assets/* + */vendor/* + */node_modules/* +