Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ All notable changes are documented here. The format is based on [Keep a Changelo
- Added `new --database=none|mariadb|mysql|pgsql|sqlite` and its question: a server database gets the service `database` in `docker-compose.yml` (`mysql:8.4`, `mariadb:11.4` or `postgres:18`, with a health check and a named volume), the PDO extension in the Dockerfile, and the connection values in `.env`; `sqlite` gets the file `storage/database.sqlite`. Each flavor gets a `DbProvider` that registers the `db` service, so `make:model` reads the database with no manual step. The service reads each value from `getenv()`, then `$_ENV`, then a default for PHP on the host. Added `Crest\Generator\Project\DatabaseChoice`, the project stubs `project-provider-db` and `project-provider-db-sqlite` (ADR and MVC), and the shared project stubs `project-compose-mysql`, `project-compose-mariadb`, `project-compose-pgsql`, `project-env-database` and `project-env-sqlite`. [#54](https://github.com/phalcon/crest/issues/54)
- Added `phalcon/phql` to the `composer.json` of an MVC project with Phalcon v6 and a database. [#54](https://github.com/phalcon/crest/issues/54)
- Added the `models` key to the `crest.php` that `new` and `init` write for an MVC project. `new --camel-case` gives `'camelCase' => true`, and `new` asks for it for an MVC project with a database; `init` writes `false`. [#54](https://github.com/phalcon/crest/issues/54)
- Added the `models` key to `config:show` in an MVC project: a `camelCase` row with its origin, and a `TABLE CLASS` table with each line of `models.classes`, sorted by table. A wrong key gives one `models` row with the error; `config:show` still exits with 0, and `make:model` stops on the error. Added `Crest\Generator\ModelNames::camelCase()`. [#71](https://github.com/phalcon/crest/issues/71)
- Added a `Note` line to `make:model` for a table whose class comes from `models.classes`: it names the old model and base, which crest keeps, and the related tables to run `make:model` for again. [#71](https://github.com/phalcon/crest/issues/71)

### Changed

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ return [
`models` changes the names that `make:model` gives in an MVC project: `classes`
gives a class to a table, and `camelCase` gives camelCase property names. `crest new`
and `crest init` write the key for an MVC project, with `'camelCase' => false`;
`crest new --camel-case` gives `true`. See "Names" in [docs/index.md](docs/index.md):
`crest new --camel-case` gives `true`. `crest config:show` shows the key. See
"Names" in [docs/index.md](docs/index.md):

```php
return [
Expand Down
46 changes: 38 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,20 +661,50 @@ return [
];
```

In an MVC project, `crest config:show` shows the key: the row `camelCase`, with
its origin, and a table with each line of `classes`, sorted by table:

ITEM VALUE ORIGIN
...
service app default
camelCase true declared

PATH LOCATION ORIGIN
...

TABLE CLASS
user-profiles UserProfile
users User

When the key is not correct, `config:show` shows the error in a `models` row,
and `make:model` stops on it:

service app default
models crest.php: models.camelCase must be true or false declared

**Classes.** `classes` gives a class to a table. Write a line only for each
table whose class you want to change: the other tables keep the class of their
name (`users` gives `Users`). The table name must be exact, with the same case.
A line also gives a class to a table whose name gives none (`user-profiles`), or
gives a name that PHP reserves (`list`), also with `--all`. With `--all`, a line
for a table that the database does not have gives a note:
name. Crest does not make a singular class name: `users` gives `Users`, not
`User`. For `User`, write the line `'users' => 'User'`. The table name must be
exact, with the same case. A line also gives a class to a table whose name gives
none (`user-profiles`), or gives a name that PHP reserves (`list`), also with
`--all`. With `--all`, a line for a table that the database does not have gives a
note:

Note: models.classes names the table Users, which the database does not have; did you mean users?

When you add a line for a table that has a model, crest writes a new base and a
new model (`UserBase.php`, `User.php`), and keeps the old files. Move your code
to the new model, delete the old files, and run `make:model` for the tables that
have relations with the table. The new base does not get the `PropertyName`
attributes of the old base: give the names again with `--property`.
new model (`UserBase.php`, `User.php`), and keeps the old files. A note names
the old files, and the tables that have relations with the table:

Note: the old files of users stay: src/Models/Users.php and src/Models/Base/UsersBase.php; move your code to User, delete them, then run make:model for invoices

Move your code to the new model, delete the old files, and run `make:model` for
the tables of the note. Crest prints the note on each run until you delete the
old files. With `--all`, the note names only the related tables that the run
does not write, such as the tables of `--exclude`. The new base does not get the
`PropertyName` attributes of the old base: give the names again with
`--property`.

**camelCase.** With `'camelCase' => true`, each property gets the camelCase form
of its column: crest splits the column at each `_`, makes the first letter of
Expand Down
88 changes: 77 additions & 11 deletions src/Command/Config/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,32 @@
namespace Crest\Command\Config;

use Crest\Command\ProjectCommand;
use Crest\Console\Exceptions\Exception;
use Crest\Console\Input;
use Crest\Console\Output;
use Crest\Console\Parsing\Definition;
use Crest\Generator\ModelNames;
use Crest\Project\Config;
use Crest\Project\Flavor;
use Crest\Project\Locator;

use function is_array;
use function ksort;
use function sort;
use function var_export;

use const SORT_STRING;

/**
* The configuration crest resolved for this project.
*
* Printing the values without saying where each came from would answer the
* easy half of the question: the useful part is knowing which of them the
* project actually asked for, and which took a default.
*
* In an MVC project, the report also shows the models key (#71): camelCase
* with its origin, and each line of models.classes. A wrong key gives a row
* with the error, so the report never stops on it.
*/
final class ShowCommand extends ProjectCommand
{
Expand All @@ -50,17 +61,24 @@ public function handle(Input $input, Output $output): int

$runtime = $config->runtime();

$output->table(
['ITEM', 'VALUE', 'ORIGIN'],
[
['root', $config->root(), Locator::FILENAME],
['flavor', $config->flavor()->value, $this->origin($config, 'flavor')],
['namespace', $config->namespace(), $this->origin($config, 'namespace')],
['bootstrap', $config->bootstrap() ?? 'none', $this->origin($config, 'bootstrap')],
['runtime', $runtime->type, $this->origin($config, 'runtime')],
['service', $runtime->service, $this->origin($config, 'runtime.service')],
]
);
$items = [
['root', $config->root(), Locator::FILENAME],
['flavor', $config->flavor()->value, $this->origin($config, 'flavor')],
['namespace', $config->namespace(), $this->origin($config, 'namespace')],
['bootstrap', $config->bootstrap() ?? 'none', $this->origin($config, 'bootstrap')],
['runtime', $runtime->type, $this->origin($config, 'runtime')],
['service', $runtime->service, $this->origin($config, 'runtime.service')],
];

$classes = [];

if (Flavor::MVC === $config->flavor()) {
[$row, $classes] = $this->models($config);

$items[] = $row;
}

$output->table(['ITEM', 'VALUE', 'ORIGIN'], $items);

$paths = $config->paths();
ksort($paths);
Expand All @@ -73,6 +91,11 @@ public function handle(Input $input, Output $output): int
$output->line();
$output->table(['PATH', 'LOCATION', 'ORIGIN'], $rows);

if ([] !== $classes) {
$output->line();
$output->table(['TABLE', 'CLASS'], $classes);
}

return 0;
}

Expand All @@ -81,6 +104,49 @@ protected function flavors(): array
return Flavor::cases();
}

/**
* The models key of an MVC project (#71): the camelCase row, and a row
* for each line of models.classes, sorted by table. The origin comes from
* the key, because ModelNames does not keep it. A wrong key gives one
* models row with the error, and no lines: make:model stops on the error,
* but config:show does not.
*
* @return array{list<string>, list<list<string>>}
*/
private function models(Config $config): array
{
$models = $config->models();

try {
$names = ModelNames::of($models);
} catch (Exception $exception) {
return [['models', $exception->getMessage(), self::DECLARED], []];
}

// mapped() gives each table as a string: PHP makes a numeric table an
// integer key.
$tables = $names->mapped();
sort($tables, SORT_STRING);

$rows = [];

foreach ($tables as $table) {
$rows[] = [$table, $names->classOf($table)];
}

// A null value counts as no value, as in ModelNames::of() (#53).
$declared = true === is_array($models) && true === isset($models['camelCase']);

return [
[
'camelCase',
var_export($names->camelCase(), true),
true === $declared ? self::DECLARED : self::DEFAULTED,
],
$rows,
];
}

private function origin(Config $config, string $key): string
{
return true === $config->isDeclared($key) ? self::DECLARED : self::DEFAULTED;
Expand Down
109 changes: 105 additions & 4 deletions src/Command/Make/ModelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
use Crest\Console\Input;
use Crest\Console\Output;
use Crest\Console\Parsing\Definition;
use Crest\Generator\ArtifactWriter;
use Crest\Generator\BaseAttributes;
use Crest\Generator\ClassName;
use Crest\Generator\ForeignKeys;
use Crest\Generator\ModelNames;
use Crest\Generator\ModelProperties;
Expand All @@ -39,13 +41,17 @@
use function array_values;
use function count;
use function explode;
use function file_get_contents;
use function implode;
use function in_array;
use function is_file;
use function max;
use function sprintf;
use function str_contains;
use function str_pad;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;
use function trim;
use function var_export;
Expand All @@ -71,7 +77,8 @@
* a PropertyName attribute on the base, or models.camelCase. A property that
* does not have the name of its column gives a columnMap() in the base. When
* a property name changes, crest says so, and names the tables whose
* relations use it.
* relations use it. When a line of models.classes gives a table a new class,
* the old model and base stay, and crest names them in a Note line (#71).
*
* The work for one table has two phases. The read phase reads the columns,
* the base, the names and the relations, and changes no file. The write phase
Expand All @@ -94,6 +101,7 @@
* map: array<string, string>,
* names: array<string, string>,
* namespace: string,
* note: string|null,
* properties: array<string, string>,
* relations: ModelRelations,
* renamed: array<string, array{string, string}>|null,
Expand All @@ -116,6 +124,15 @@ final class ModelCommand extends ProjectCommand
private const NO_KEYS = 'No relations for %s: the table has no foreign keys, and no foreign keys point to it. '
. 'Relations come only from foreign keys.';

/**
* The Note line for the old files of a table whose class now comes from
* models.classes (#71): with only the old base, and with the old model
* and the old base. The last %s is the end of the line.
*/
private const OLD_FILE = 'Note: the old file of %s stays: %s; move your code to %s, delete it%s';

private const OLD_FILES = 'Note: the old files of %s stay: %s and %s; move your code to %s, delete them%s';

/**
* The stubs that a base with a columnMap() needs, with the placeholder
* of #53 in each one.
Expand Down Expand Up @@ -166,6 +183,18 @@ private static function bases(ModelNames $names, array $tables, string $path): a
return [$names, $bases];
}

/**
* The class of the table, or null for a table that gives no class.
*/
private static function classOrNull(ModelNames $names, string $table): ?string
{
try {
return $names->classOf($table);
} catch (Exception) {
return null;
}
}

/**
* The names of --property: column => name (spec 3.1). Each item is split
* at its last `=`, and the spaces at the start and the end of each part
Expand Down Expand Up @@ -217,6 +246,73 @@ private static function mapped(array $names): bool
return false;
}

/**
* The Note line for the old files of the table (#71), or null. The class
* of the table comes from models.classes, so crest writes a new model and
* a new base, and the old files stay. There is a line only when:
*
* 1. the table gives a default class;
* 2. no table gives the default class, without regard to case. The
* tables are the tables of models.classes and of the database, and
* ForeignKeys::read() puts the table itself in them. When the table
* itself gives the default class, there are no old files: PHP sees
* User and USER as one class, and a file system that ignores case
* sees one file. When another table gives it, the old base can be
* the base of that table;
* 3. the old base exists and has ArtifactWriter::MARKER.
*
* The line also names the related tables that give a class and that this
* run does not write: their bases have relations with the old class.
*
* @param list<string> $written The tables that this run writes
*/
private static function oldFiles(
ModelNames $names,
ForeignKeys $keys,
string $table,
string $class,
string $path,
array $written
): ?string {
try {
$default = ClassName::fromTable($table);
} catch (Exception) {
return null;
}

foreach ([...$names->mapped(), ...$keys->tables()] as $other) {
$found = self::classOrNull($names, $other);

if (null !== $found && strtolower($found) === strtolower($default)) {
return null;
}
}

// Suppressed: a missing or unreadable old base gives no line. The
// line is information, so it does not stop the command.
$base = self::baseFile($path, $default);
$contents = @file_get_contents($base);

if (false === $contents || false === str_contains($contents, ArtifactWriter::MARKER)) {
return null;
}

$run = [];

foreach (array_diff($keys->related($table), $written) as $other) {
if (null !== self::classOrNull($names, $other)) {
$run[] = $other;
}
}

$then = [] === $run ? '' : ', then run make:model for ' . Sentence::join($run);
$model = $path . '/' . $default . '.php';

return true === is_file($model)
? sprintf(self::OLD_FILES, $table, $model, $base, $class, $then)
: sprintf(self::OLD_FILE, $table, $base, $class, $then);
}

/**
* The keys of the array, as strings: PHP makes a numeric key an integer.
*
Expand Down Expand Up @@ -588,6 +684,7 @@ private function read(
'map' => true === $mapped ? $final : [],
'names' => $final,
'namespace' => $namespace,
'note' => self::oldFiles($names, $keys, $table, $class, $path, $written),
'properties' => $columns,
'relations' => $relations,
'renamed' => $renamed,
Expand Down Expand Up @@ -751,11 +848,15 @@ private function write(Config $config, array $parts, bool $force, Output $output

if (Outcome::Kept === $outcome) {
$output->line(sprintf('Kept %s: it exists; pass --force to replace it', $parts['file']));

return $outcome;
} else {
$output->success(sprintf('Created %s', $parts['file']));
}

$output->success(sprintf('Created %s', $parts['file']));
// The old files of the table stay: the developer moves the code and
// deletes them (#71).
if (null !== $parts['note']) {
$output->line($parts['note']);
}

return $outcome;
}
Expand Down
Loading
Loading