diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 167cf4e46..5282b6915 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: jobs: testsuite: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: @@ -151,7 +151,7 @@ jobs: cs-stan: name: Coding Standard & Static Analysis - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -168,7 +168,7 @@ jobs: run: composer stan-setup - name: Run phpcs - run: vendor/bin/phpcs --report=checkstyle src/ tests/ | cs2pr + run: vendor/bin/phpcs --report=checkstyle | cs2pr - name: Run psalm if: success() || failure() diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..69e7d139d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,29 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 0 * * *" + +permissions: + contents: read + +jobs: + stale: + + permissions: + issues: write # for actions/stale to close stale issues + pull-requests: write # for actions/stale to close stale PRs + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v8 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days' + stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + days-before-stale: 120 + days-before-close: 15 + exempt-issue-labels: 'pinned' + exempt-pr-labels: 'pinned' diff --git a/composer.json b/composer.json index 58ad76d07..7dda5014e 100644 --- a/composer.json +++ b/composer.json @@ -50,9 +50,9 @@ "@test", "@cs-check" ], - "cs-check": "phpcs --parallel=16 -p src/ tests/", - "cs-fix": "phpcbf --parallel=16 -p src/ tests/", - "stan": "phpstan analyse src/ && psalm.phar", + "cs-check": "phpcs -p --parallel=16", + "cs-fix": "phpcbf -p --parallel=16", + "stan": "phpstan analyse && psalm.phar", "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^1.7 psalm/phar:~4.27.0 && mv composer.backup composer.json", "test": "phpunit", "test-coverage": "phpunit --coverage-clover=clover.xml" diff --git a/docs.Dockerfile b/docs.Dockerfile index 1f7afabdc..4f3ca473c 100644 --- a/docs.Dockerfile +++ b/docs.Dockerfile @@ -1,5 +1,5 @@ # Generate the HTML output. -FROM markstory/cakephp-docs-builder as builder +FROM ghcr.io/cakephp/docs-builder as builder # Copy entire repo in with .git so we can build all versions in one image. COPY docs /data/docs @@ -8,7 +8,7 @@ RUN cd /data/docs-builder \ && make website LANGS="en es fr ja pt ru" SOURCE=/data/docs DEST=/data/website/ # Build a small nginx container with just the static site in it. -FROM markstory/cakephp-docs-builder:runtime as runtime +FROM ghcr.io/cakephp/docs-builder:runtime as runtime # Configure search index script ENV LANGS="en es fr ja pt ru" diff --git a/docs/en/development.rst b/docs/en/development.rst index 735c4183f..2efb76b2a 100644 --- a/docs/en/development.rst +++ b/docs/en/development.rst @@ -226,7 +226,7 @@ dependencies in your templates you can include template overrides in your application templates. These overrides work similar to overriding other plugin templates. -#. Create a new directory **/templates/plugin/Bake/**. +#. Create a new directory **/templates/plugin/Bake/bake/**. #. Copy any templates you want to override from **vendor/cakephp/bake/templates/bake/** to matching files in your application. diff --git a/phpcs.xml.dist b/phpcs.xml similarity index 52% rename from phpcs.xml.dist rename to phpcs.xml index ce02df237..dda905e36 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml @@ -1,8 +1,14 @@ + + + src/ + tests/ */comparisons/* + /tests/test_app/tests/ + /tests/test_app/Plugin/TestBake/ diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index 8c72cef32..fc9b95ec7 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -233,7 +233,7 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a ]; $primary = $table->getPrimaryKey(); - $associations = $this->findBelongsTo($table, $associations); + $associations = $this->findBelongsTo($table, $associations, $args); if (is_array($primary) && count($primary) > 1) { $io->warning( @@ -329,9 +329,10 @@ public function getAssociationInfo(Table $table): array * * @param \Cake\ORM\Table $model Database\Table instance of table being generated. * @param array $associations Array of in progress associations + * @param \Cake\Console\Arguments|null $args CLI arguments * @return array Associations with belongsTo added in. */ - public function findBelongsTo(Table $model, array $associations): array + public function findBelongsTo(Table $model, array $associations, ?Arguments $args = null): array { $schema = $model->getSchema(); foreach ($schema->columns() as $fieldName) { @@ -362,11 +363,13 @@ public function findBelongsTo(Table $model, array $associations): array get_class($associationTable) === Table::class && !in_array(Inflector::tableize($tmpModelName), $tables, true) ) { + $allowAliasRelations = $args && $args->getOption('skip-relation-check'); $found = $this->findTableReferencedBy($schema, $fieldName); - if (!$found) { + if ($found) { + $tmpModelName = Inflector::camelize($found); + } elseif (!$allowAliasRelations) { continue; } - $tmpModelName = Inflector::camelize($found); } $assoc = [ 'alias' => $tmpModelName, @@ -1319,6 +1322,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar ])->addOption('no-fixture', [ 'boolean' => true, 'help' => 'Do not generate a test fixture skeleton.', + ])->addOption('skip-relation-check', [ + 'boolean' => true, + 'help' => 'Generate relations for all "example_id" fields' + . ' without checking the database if a table "examples" exists.', ])->setEpilog( 'Omitting all arguments and options will list the table names you can generate models for.' ); diff --git a/src/Command/PluginCommand.php b/src/Command/PluginCommand.php index 5e12bb471..992ba2ee9 100644 --- a/src/Command/PluginCommand.php +++ b/src/Command/PluginCommand.php @@ -383,7 +383,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar ])->addOption('theme', [ 'short' => 't', 'help' => 'The theme to use when baking code.', - 'default' => Configure::read('Bake.theme') ?? '', + 'default' => Configure::read('Bake.theme') ?: null, 'choices' => $this->_getBakeThemes(), ]); diff --git a/src/View/BakeView.php b/src/View/BakeView.php index bc818f08c..3a3b6df78 100644 --- a/src/View/BakeView.php +++ b/src/View/BakeView.php @@ -107,7 +107,7 @@ public function render(?string $template = null, $layout = null): string * * @param mixed $subject The object that this event applies to * ($this by default). - * @return \Cake\Event\EventInterface + * @return \Cake\Event\EventInterface */ public function dispatchEvent(string $name, $data = null, $subject = null): EventInterface { diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index e7c5c6791..3080f58e7 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -490,6 +490,59 @@ public function testGetAssociationsAddAssociationIfTableExist() $this->assertEquals($expected, $result); } + /** + * Test that association generation adds `Anythings` association for `anything_id` field + * when using `--skip-relation-check` option, even if no db table exists + * + * @return void + */ + public function testGetAssociationsAddAssociationIfNoTableExistButAliasIsAllowed() + { + $items = $this->getTableLocator()->get('TodoItems'); + + $items->setSchema($items->getSchema()->addColumn('anything_id', ['type' => 'integer'])); + $command = new ModelCommand(); + $command->connection = 'test'; + + $args = new Arguments([], ['skip-relation-check' => true], []); + $io = $this->createMock(ConsoleIo::class); + $result = $command->getAssociations($items, $args, $io); + $expected = [ + 'belongsTo' => [ + [ + 'alias' => 'Users', + 'foreignKey' => 'user_id', + 'joinType' => 'INNER', + ], + [ + 'alias' => 'Anythings', + 'foreignKey' => 'anything_id', + ], + ], + 'hasMany' => [ + [ + 'alias' => 'TodoTasks', + 'foreignKey' => 'todo_item_id', + ], + ], + 'belongsToMany' => [ + [ + 'alias' => 'TodoLabels', + 'foreignKey' => 'todo_item_id', + 'joinTable' => 'todo_items_todo_labels', + 'targetForeignKey' => 'todo_label_id', + ], + ], + 'hasOne' => [ + [ + 'alias' => 'TodoReminders', + 'foreignKey' => 'todo_item_id', + ], + ], + ]; + $this->assertEquals($expected, $result); + } + /** * Test that association generation ignores `_id` fields * diff --git a/tests/TestCase/Command/PluginCommandTest.php b/tests/TestCase/Command/PluginCommandTest.php index 03d0858f8..5aab14ebb 100644 --- a/tests/TestCase/Command/PluginCommandTest.php +++ b/tests/TestCase/Command/PluginCommandTest.php @@ -34,6 +34,8 @@ class PluginCommandTest extends TestCase { protected $testAppFile = APP . 'Application.php'; + protected $pluginsPath = TMP . 'plugin_task' . DS; + /** * setUp method * @@ -47,11 +49,10 @@ public function setUp(): void $this->useCommandRunner(); // Output into a safe place. - $path = TMP . 'plugin_task' . DS; - Configure::write('App.paths.plugins', [$path]); + Configure::write('App.paths.plugins', [$this->pluginsPath]); // Create the test output path - mkdir($path, 0777, true); + mkdir($this->pluginsPath, 0777, true); if (file_exists(APP . 'Application.php.bak')) { rename(APP . 'Application.php.bak', APP . 'Application.php'); @@ -68,7 +69,7 @@ public function setUp(): void public function tearDown(): void { $fs = new Filesystem(); - $fs->deleteDir(TMP . 'plugin_task'); + $fs->deleteDir($this->pluginsPath); if (file_exists(APP . 'Application.php.bak')) { rename(APP . 'Application.php.bak', APP . 'Application.php'); @@ -89,6 +90,16 @@ public function testMainBakePluginContents() $this->assertPluginContents('SimpleExample'); } + public function testBakingWithNonExistentPluginsDir() + { + $fs = new Filesystem(); + $fs->deleteDir($this->pluginsPath); + + $this->exec('bake plugin SimpleExample', ['y', 'n']); + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertPluginContents('SimpleExample'); + } + /** * test creating a plugin with a custom app namespace. * @@ -211,7 +222,7 @@ public function testFindPathNonExistent() $result = $command->findPath($paths, $io); $this->assertNull($result, 'no return'); - $this->assertSame(TMP . 'plugin_task' . DS, $command->path); + $this->assertSame($this->pluginsPath, $command->path); } /**