Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
cc164f5
:sparkles: Add work from the other branch
gturpin-dev Oct 30, 2024
cfc7ad3
:art: Coding styles
gturpin-dev Oct 30, 2024
f30cefa
:rotating_light: Run phpstan
gturpin-dev Oct 30, 2024
a7aa94e
:rotating_light: Run Rector
gturpin-dev Oct 30, 2024
c4dee0a
:sparkles: Add the make:model command
gturpin-dev Oct 30, 2024
030e2be
:art: Coding styles
gturpin-dev Oct 30, 2024
a3e8536
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
a6ef03c
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
ef7d478
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
8654725
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
9e33d9d
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
084cf21
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
c5c7b20
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
98f87e2
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
8b2f05c
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
5070533
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
af7ed2a
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
bc9f939
:pencil: Update phpdoc and code styles
gturpin-dev Nov 4, 2024
a31d41d
:sparkles: Refactor and add manipulations on the fly
gturpin-dev Nov 4, 2024
5509af0
:bug: Fix generation aliasing old classname
gturpin-dev Nov 5, 2024
1405419
:recycle: Refactor PublishesFiles to use the StubFileGenerator
gturpin-dev Nov 6, 2024
ef37a84
:recycle: Refactor make:controller command to work with new methods
gturpin-dev Nov 6, 2024
1a6ca14
:recycle: Refactor namespace build for generation
gturpin-dev Nov 6, 2024
9f8f804
:rotating_light: Run rector
gturpin-dev Nov 6, 2024
030bef7
:rotating_light: Run phpstan
gturpin-dev Nov 6, 2024
a4ad57c
:art: Coding styles
gturpin-dev Nov 6, 2024
1ef9f60
:fire: Remove unused exception
gturpin-dev Nov 6, 2024
08a4783
:white_check_mark: Add tests for PathHelper::toClassName
gturpin-dev Nov 7, 2024
c23c9bf
:fire: make:model now only create a database model
gturpin-dev Nov 7, 2024
ea917b7
:fire: Removing pathPrefix and classSufix on make commands
gturpin-dev Nov 7, 2024
c69ce91
:art: Move make:controller command to tempest/http package
gturpin-dev Nov 7, 2024
d6dfb86
:art: Move make:model command to tempest/database package
gturpin-dev Nov 7, 2024
e074fa3
:recycle: Refactor HasGeneratorCommand to use Injected properties
gturpin-dev Nov 7, 2024
9698266
:recycle: Rename HasGeneratorCommand trait to HasGeneratorConsoleInte…
gturpin-dev Nov 7, 2024
0891b57
qa
brendt Nov 7, 2024
4b0dc40
Update phpstan
brendt Nov 7, 2024
3b7794e
:art: Fix coding styles
gturpin-dev Nov 7, 2024
5df1b1f
:recycle: Refactor StubFile accessor to improve the API
gturpin-dev Nov 7, 2024
9006d9c
:recycle::construction: Start refactoring StubFileGenerator to avoid …
gturpin-dev Nov 7, 2024
a6a071b
:bug: Fix StubFile issue
gturpin-dev Nov 7, 2024
3d0408f
:art: Coding styles
gturpin-dev Nov 7, 2024
e4b9360
Merge branch 'main' into feat/console/make-commands
brendt Nov 8, 2024
5a7c1b1
wip
brendt Nov 8, 2024
62c254f
qa
brendt Nov 8, 2024
de86401
wip
brendt Nov 8, 2024
9601ee9
Add some tests
brendt Nov 8, 2024
39a42ae
:recycle: Replace DIRECTORY_SEPARATOR with slashes
gturpin-dev Nov 8, 2024
9bb7ca1
:white_check_mark: Add tests
gturpin-dev Nov 8, 2024
a7f1ed7
:art: Coding styles
gturpin-dev Nov 8, 2024
84b88a5
:white_check_mark: Add tests for PublishesFiles trait
gturpin-dev Nov 8, 2024
398dc80
:white_check_mark: Add test for make:model command
gturpin-dev Nov 8, 2024
8f6f985
:test_tube: Add failing test for PublishesFiles
gturpin-dev Nov 8, 2024
9418d92
:bug: Fix the failing test
gturpin-dev Nov 12, 2024
76e27b5
:recycle: Refactor the make:controller tests
gturpin-dev Nov 12, 2024
854984e
Merge branch 'main' into feat/console/make-commands
gturpin-dev Nov 12, 2024
2d6e512
:art: Coding styles
gturpin-dev Nov 12, 2024
99f4821
:bug: Try fixing windows paths CI issue
gturpin-dev Nov 12, 2024
b39c42c
:art: Coding styles
gturpin-dev Nov 12, 2024
7a7ab00
Merge branch 'main' into feat/console/make-commands
gturpin-dev Nov 13, 2024
92b4b88
Merge branch 'main' into feat/console/make-commands
brendt Nov 14, 2024
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
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ parameters:
path: src/Tempest/Console/src/Terminal/Terminal.php
-
message: '#.*uninitialized readonly property \$console*#'
-
message: '#.*uninitialized readonly property \$composer*#'
-
message: '#.*uninitialized readonly property \$stubFileGenerator*#'

disallowedFunctionCalls:
-
Expand Down
2 changes: 1 addition & 1 deletion src/Tempest/Auth/src/Install/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function __construct(
) {
}

public function matches(string|UnitEnum|Permission $match): bool
public function matches(string|UnitEnum|self $match): bool
{
$match = match(true) {
is_string($match) => $match,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function render(Console $console): ?string
return $this->default;
}

$console->write("<question>{$this->label}</question> ");
$console->write("<question>{$this->label}</question> ({$this->default})");

return trim($console->readln()) ?: $this->default;
}
Expand Down
171 changes: 125 additions & 46 deletions src/Tempest/Core/src/PublishesFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,110 @@
namespace Tempest\Core;

use Closure;
use Nette\InvalidStateException;
use Tempest\Console\HasConsole;
use Tempest\Container\Inject;
use Tempest\Generation\ClassManipulator;
use function Tempest\src_namespace;
use function Tempest\src_path;
use Tempest\Generation\DataObjects\StubFile;
use Tempest\Generation\Enums\StubFileType;
use Tempest\Generation\Exceptions\FileGenerationAbortedException;
use Tempest\Generation\Exceptions\FileGenerationFailedException;
use Tempest\Generation\StubFileGenerator;
use Tempest\Support\PathHelper;
use function Tempest\Support\str;
use Tempest\Validation\Rules\EndsWith;
use Tempest\Validation\Rules\NotEmpty;
use Throwable;

/**
* Provides a bunch of methods to publish and generate files and work with common user input.
*/
trait PublishesFiles
{
use HasConsole;

#[Inject]
private readonly Composer $composer;

#[Inject]
private readonly StubFileGenerator $stubFileGenerator;

private array $publishedFiles = [];

private array $publishedClasses = [];

/**
* @param Closure(string $source, string $destination): void|null $callback
* Publishes a file from a source to a destination.
* @param string $source The path to the source file.
* @param string $destination The path to the destination file.
* @param Closure(string $source, string $destination): void|null $callback A callback to run after the file is published.
*/
public function publish(
string $source,
string $destination,
?Closure $callback = null,
): void {
if (file_exists($destination)) {
if (! $this->confirm(
question: "{$destination} already exists Do you want to overwrite it?",
)) {
return;
}
} else {
if (! $this->confirm(
question: "Do you want to create {$destination}?",
try {
if (! $this->console->confirm(
question: sprintf('Do you want to create "%s"', $destination),
default: true,
)) {
$this->writeln('Skipped');
throw new FileGenerationAbortedException('Skipped.');
}

return;
if (! $this->askForOverride($destination)) {
throw new FileGenerationAbortedException('Skipped.');
}
}

$dir = pathinfo($destination, PATHINFO_DIRNAME);
$stubFile = StubFile::from($source);

if (! is_dir($dir)) {
mkdir($dir, recursive: true);
}
// Handle class files
if ($stubFile->type === StubFileType::CLASS_FILE) {
$oldClass = new ClassManipulator($source);

copy($source, $destination);
$this->stubFileGenerator->generateClassFile(
stubFile: $stubFile,
targetPath: $destination,
shouldOverride: true,
manipulations: [
fn (ClassManipulator $class) => $class->removeClassAttribute(DoNotDiscover::class),
],
);

$this->updateClass($destination);
$newClass = new ClassManipulator($destination);

$this->publishedFiles[] = $destination;
$this->publishedClasses[$oldClass->getClassName()] = $newClass->getClassName();
}

if ($callback !== null) {
$callback($source, $destination);
}
// Handle raw files
if ($stubFile->type === StubFileType::RAW_FILE) {
$this->stubFileGenerator->generateRawFile(
stubFile: $stubFile,
targetPath: $destination,
shouldOverride: true,
);
}

$this->publishedFiles[] = $destination;

$this->success("{$destination} created");
if ($callback !== null) {
$callback($source, $destination);
}

$this->console->success(sprintf('File successfully created at "%s".', $destination));
} catch (FileGenerationAbortedException $exception) {
$this->console->info($exception->getMessage());
} catch (Throwable $throwable) {
throw new FileGenerationFailedException(
message: 'The file could not be published.',
previous: $throwable,
);
}
}

/**
* Publishes the imports of the published classes.
* Any published class that is imported in another published class will have its import updated.
*/
public function publishImports(): void
{
foreach ($this->publishedFiles as $file) {
Expand All @@ -77,30 +122,64 @@ public function publishImports(): void
}
}

private function updateClass(string $destination): void
/**
* Gets a suggested path for the given class name.
* This will use the user's main namespace as the base path.
* @param string $className The class name to generate the path for, can include path parts (e.g. 'Models/User').
* @param string|null $pathPrefix The prefix to add to the path (e.g. 'Models').
* @param string|null $classSuffix The suffix to add to the class name (e.g. 'Model').
* @return string The fully suggested path including the filename and extension.
*/
public function getSuggestedPath(string $className, ?string $pathPrefix = null, ?string $classSuffix = null): string
{
try {
$class = new ClassManipulator($destination);
} catch (InvalidStateException) {
return;
}
// Separate input path and classname
$inputClassName = PathHelper::toClassName($className);
$inputPath = str(PathHelper::make($className))->replaceLast($inputClassName, '')->toString();
$className = str($inputClassName)
->pascal()
->finish($classSuffix ?? '')
->toString();

$namespace = str($destination)
->replaceStart(rtrim(src_path(), '/'), src_namespace())
->replaceEnd('.php', '')
->replace('/', '\\')
->explode('\\')
->pop($value)
->implode('\\')
// Prepare the suggested path from the project namespace
return str(PathHelper::make(
$this->composer->mainNamespace->path,
$pathPrefix ?? '',
$inputPath,
))
->finish('/')
->append($className . '.php')
->toString();
}

/**
* Prompt the user for the target path to save the generated file.
* @param string $suggestedPath The suggested path to show to the user.
* @return string The target path that the user has chosen.
*/
public function promptTargetPath(string $suggestedPath): string
{
$className = PathHelper::toClassName($suggestedPath);

$oldClassName = $class->getClassName();
return $this->console->ask(
question: sprintf('Where do you want to save the file "%s"?', $className),
default: $suggestedPath,
validation: [new NotEmpty(), new EndsWith('.php')],
);
}

$class
->setNamespace($namespace)
->removeClassAttribute(DoNotDiscover::class)
->save($destination);
/**
* Ask the user if they want to override the file if it already exists.
* @param string $targetPath The target path to check for existence.
* @return bool Whether the user wants to override the file.
*/
public function askForOverride(string $targetPath): bool
{
if (! file_exists($targetPath)) {
return true;
}

$this->publishedClasses[$oldClassName] = $class->getClassName();
return $this->console->confirm(
question: sprintf('The file "%s" already exists. Do you want to override it?', $targetPath),
);
}
}
47 changes: 47 additions & 0 deletions src/Tempest/Database/src/Commands/MakeModelCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Commands;

use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Core\PublishesFiles;
use Tempest\Database\Stubs\DatabaseModelStub;
use Tempest\Generation\DataObjects\StubFile;
use Tempest\Generation\Exceptions\FileGenerationAbortedException;
use Tempest\Generation\Exceptions\FileGenerationFailedException;

final class MakeModelCommand
{
use PublishesFiles;

#[ConsoleCommand(
name: 'make:model',
description: 'Creates a new model class',
aliases: ['model:make', 'model:create', 'create:model'],
)]
public function __invoke(
#[ConsoleArgument(
help: 'The name of the model class to create',
)]
string $className,
): void {
$suggestedPath = $this->getSuggestedPath($className);
$targetPath = $this->promptTargetPath($suggestedPath);
$shouldOverride = $this->askForOverride($targetPath);

try {

$this->stubFileGenerator->generateClassFile(
stubFile: StubFile::from(DatabaseModelStub::class),
targetPath: $targetPath,
shouldOverride: $shouldOverride,
);

$this->console->success(sprintf('File successfully created at "%s".', $targetPath));
} catch (FileGenerationAbortedException|FileGenerationFailedException $e) {
$this->console->error($e->getMessage());
}
}
}
20 changes: 20 additions & 0 deletions src/Tempest/Database/src/Stubs/DatabaseModelStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Stubs;

use Tempest\Database\DatabaseModel;
use Tempest\Database\IsDatabaseModel;
use Tempest\Validation\Rules\Length;

final class DatabaseModelStub implements DatabaseModel
{
use IsDatabaseModel;

public function __construct(
#[Length(min: 1, max: 120)]
public string $title
) {
}
}
14 changes: 13 additions & 1 deletion src/Tempest/Generation/src/ClassManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\PhpFile;
use ReflectionClass;
use Tempest\Generation\Exceptions\FileGenerationFailedException;

final class ClassManipulator
{
Expand All @@ -29,6 +30,13 @@ public function __construct(string|ReflectionClass $source)
$this->namespace = $this->classType->getNamespace()->getName();
}

/**
* Save the class to a target file.
*
* @param string $path the path to save the class to.
*
* @throws FileGenerationFailedException if the file could not be written.
*/
public function save(string $path): self
{
$dir = pathinfo($path, PATHINFO_DIRNAME);
Expand All @@ -37,7 +45,11 @@ public function save(string $path): self
mkdir($dir, recursive: true);
}

file_put_contents($path, $this->print());
$isSuccess = (bool) file_put_contents($path, $this->print());

if (! $isSuccess) {
throw new FileGenerationFailedException(sprintf('The file "%s" could not be written.', $path));
}

return $this;
}
Expand Down
Loading
Loading