├── composer.json └── src ├── Commands ├── GeneratorCommand.php └── MigrationGeneratorCommand.php ├── Concerns ├── CodeGenerator.php ├── CreatesUsingGeneratorPreset.php ├── MigrationGenerator.php ├── ResolvesPresetStubs.php ├── TestGenerator.php └── UsesGeneratorOverrides.php ├── Contracts └── GeneratesCode.php ├── LaravelServiceProvider.php ├── PresetManager.php └── Presets ├── Laravel.php └── Preset.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://getcomposer.org/schema.json", 3 | "name": "orchestra/canvas-core", 4 | "description": "Code Generators Builder for Laravel Applications and Packages", 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Taylor Otwell", 10 | "email": "taylor@laravel.com" 11 | }, 12 | { 13 | "name": "Mior Muhammad Zaki", 14 | "email": "crynobone@gmail.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Orchestra\\Canvas\\Core\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Orchestra\\Canvas\\Core\\Tests\\": "tests/", 25 | "Workbench\\App\\": "workbench/app/" 26 | } 27 | }, 28 | "require": { 29 | "php": "^8.2", 30 | "composer-runtime-api": "^2.2", 31 | "composer/semver": "^3.0", 32 | "illuminate/console": "^12.1.1", 33 | "illuminate/support": "^12.1.1", 34 | "orchestra/sidekick": "^1.2.0", 35 | "symfony/polyfill-php83": "^1.32" 36 | }, 37 | "require-dev": { 38 | "laravel/framework": "^12.0", 39 | "laravel/pint": "^1.22", 40 | "mockery/mockery": "^1.6.10", 41 | "orchestra/testbench-core": "^10.1.0", 42 | "phpstan/phpstan": "^2.1.14", 43 | "phpunit/phpunit": "^11.5.12|^12.0.1", 44 | "spatie/laravel-ray": "^1.40.2", 45 | "symfony/yaml": "^7.2" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "support": { 51 | "issues": "https://github.com/orchestral/canvas/issues" 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "Orchestra\\Canvas\\Core\\LaravelServiceProvider" 57 | ] 58 | } 59 | }, 60 | "scripts": { 61 | "post-autoload-dump": [ 62 | "@clear", 63 | "@prepare" 64 | ], 65 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 66 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 67 | "lint": [ 68 | "@php vendor/bin/pint --ansi", 69 | "@php vendor/bin/phpstan analyse --verbose --ansi" 70 | ], 71 | "test": "@php vendor/bin/phpunit -c ./ --color", 72 | "ci": [ 73 | "@composer audit", 74 | "@prepare", 75 | "@lint", 76 | "@test" 77 | ] 78 | }, 79 | "prefer-stable": true, 80 | "minimum-stability": "dev" 81 | } 82 | -------------------------------------------------------------------------------- /src/Commands/GeneratorCommand.php: -------------------------------------------------------------------------------- 1 | addGeneratorPresetOptions(); 26 | } 27 | 28 | /** {@inheritDoc} */ 29 | #[\Override] 30 | public function handle() 31 | { 32 | /** @phpstan-ignore return.type */ 33 | return $this->generateCode() ? self::SUCCESS : self::FAILURE; 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | #[\Override] 38 | protected function getPath($name) 39 | { 40 | return $this->getPathUsingCanvas($name); 41 | } 42 | 43 | /** {@inheritDoc} */ 44 | #[\Override] 45 | protected function qualifyModel(string $model) 46 | { 47 | return $this->qualifyModelUsingCanvas($model); 48 | } 49 | 50 | /** {@inheritDoc} */ 51 | #[\Override] 52 | protected function rootNamespace() 53 | { 54 | return $this->rootNamespaceUsingCanvas(); 55 | } 56 | 57 | /** {@inheritDoc} */ 58 | #[\Override] 59 | protected function userProviderModel() 60 | { 61 | return $this->userProviderModelUsingCanvas(); 62 | } 63 | 64 | /** {@inheritDoc} */ 65 | #[\Override] 66 | protected function viewPath($path = '') 67 | { 68 | return $this->viewPathUsingCanvas($path); 69 | } 70 | 71 | /** {@inheritDoc} */ 72 | #[\Override] 73 | protected function possibleModels() 74 | { 75 | return $this->possibleModelsUsingCanvas(); 76 | } 77 | 78 | /** {@inheritDoc} */ 79 | #[\Override] 80 | protected function possibleEvents() 81 | { 82 | return $this->possibleEventsUsingCanvas(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Commands/MigrationGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | addGeneratorPresetOptions(); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | #[\Override] 27 | protected function createBaseMigration($table) 28 | { 29 | return $this->createBaseMigrationUsingCanvas($table); 30 | } 31 | 32 | /** {@inheritDoc} */ 33 | #[\Override] 34 | protected function migrationExists($table) 35 | { 36 | return $this->migrationExistsUsingCanvas($table); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Concerns/CodeGenerator.php: -------------------------------------------------------------------------------- 1 | getNameInput(); 18 | $force = $this->hasOption('force') && $this->option('force') === true; 19 | 20 | $className = $this->qualifyClass($name); 21 | $path = $this->getPath($this->qualifyClass($name)); 22 | 23 | // First we need to ensure that the given name is not a reserved word within the PHP 24 | // language and that the class name will actually be valid. If it is not valid we 25 | // can error now and prevent from polluting the filesystem using invalid files. 26 | if ($this->isReservedName($name)) { 27 | $this->components->error(\sprintf('The name "%s" is reserved by PHP.', $name)); 28 | 29 | return false; 30 | } 31 | 32 | // Next, We will check to see if the class already exists. If it does, we don't want 33 | // to create the class and overwrite the user's code. So, we will bail out so the 34 | // code is untouched. Otherwise, we will continue generating this class' files. 35 | if (! $force && $this->alreadyExists($name)) { 36 | return $this->codeAlreadyExists($className, $path); 37 | } 38 | 39 | // Next, we will generate the path to the location where this class' file should get 40 | // written. Then, we will build the class and make the proper replacements on the 41 | // stub files so that it gets the correctly formatted namespace and class name. 42 | $this->makeDirectory($path); 43 | 44 | $this->files->put( 45 | $path, $this->sortImports($this->generatingCode($this->buildClass($className), $className)) 46 | ); 47 | 48 | if (\in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { 49 | $this->handleTestCreationUsingCanvas($path); 50 | } 51 | 52 | return tap($this->codeHasBeenGenerated($className, $path), function ($exitCode) use ($className, $path) { 53 | $this->afterCodeHasBeenGenerated($className, $path); 54 | }); 55 | } 56 | 57 | /** 58 | * Handle generating code. 59 | */ 60 | public function generatingCode(string $stub, string $className): string 61 | { 62 | return $stub; 63 | } 64 | 65 | /** 66 | * Code already exists. 67 | */ 68 | public function codeAlreadyExists(string $className, string $path): bool 69 | { 70 | $this->components->error( 71 | \sprintf( 72 | '%s [%s] already exists!', $this->type, Str::after($path, $this->generatorPreset()->basePath().DIRECTORY_SEPARATOR) 73 | ) 74 | ); 75 | 76 | return false; 77 | } 78 | 79 | /** 80 | * Code successfully generated. 81 | */ 82 | public function codeHasBeenGenerated(string $className, string $path): bool 83 | { 84 | $this->components->info( 85 | \sprintf( 86 | '%s [%s] created successfully.', $this->type, Str::after($path, $this->generatorPreset()->basePath().DIRECTORY_SEPARATOR) 87 | ) 88 | ); 89 | 90 | return true; 91 | } 92 | 93 | /** 94 | * Run after code successfully generated. 95 | */ 96 | public function afterCodeHasBeenGenerated(string $className, string $path): void 97 | { 98 | // 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Concerns/CreatesUsingGeneratorPreset.php: -------------------------------------------------------------------------------- 1 | type)) { 22 | $message = 'when generating '.Str::lower($this->type); 23 | } 24 | 25 | $this->getDefinition()->addOption(new InputOption( 26 | 'preset', 27 | null, 28 | InputOption::VALUE_OPTIONAL, 29 | \sprintf('Preset used %s', $message), 30 | null, 31 | )); 32 | } 33 | 34 | /** 35 | * Resolve the generator preset. 36 | */ 37 | protected function generatorPreset(): Preset 38 | { 39 | /** @var string|null $preset */ 40 | $preset = $this->option('preset'); 41 | 42 | return $this->laravel->make(PresetManager::class)->driver($preset); 43 | } 44 | 45 | /** 46 | * Get the generator preset source path. 47 | */ 48 | protected function getGeneratorSourcePath(): string 49 | { 50 | return $this->generatorPreset()->sourcePath(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Concerns/MigrationGenerator.php: -------------------------------------------------------------------------------- 1 | laravel->make('migration.creator')->create( 17 | "create_{$table}_table", $this->generatorPreset()->migrationPath() 18 | ); 19 | } 20 | 21 | /** 22 | * Determine whether a migration for the table already exists. 23 | */ 24 | protected function migrationExistsUsingCanvas(string $table): bool 25 | { 26 | return \count($this->files->glob( 27 | join_paths($this->generatorPreset()->migrationPath(), '*_*_*_*_create_'.$table.'_table.php') 28 | )) !== 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Concerns/ResolvesPresetStubs.php: -------------------------------------------------------------------------------- 1 | generatorPreset(); 18 | 19 | return $preset->hasCustomStubPath() && file_exists($customPath = join_paths($preset->basePath(), trim($stub, '/'))) 20 | ? $customPath 21 | : $this->resolveDefaultStubPath($stub); 22 | } 23 | 24 | /** 25 | * Resolve the default fully-qualified path to the stub. 26 | * 27 | * @param string $stub 28 | * @return string 29 | */ 30 | protected function resolveDefaultStubPath($stub) 31 | { 32 | return $stub; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Concerns/TestGenerator.php: -------------------------------------------------------------------------------- 1 | option('test') && ! $this->option('pest')) { 17 | return false; 18 | } 19 | 20 | $sourcePath = $this->generatorPreset()->sourcePath(); 21 | 22 | return $this->call('make:test', array_merge([ 23 | 'name' => Str::of($path)->after($sourcePath)->beforeLast('.php')->append('Test')->replace('\\', '/'), 24 | '--pest' => $this->option('pest'), 25 | ], array_filter([ 26 | '--preset' => $this->hasOption('preset') ? $this->option('preset') : null, 27 | ]))) == 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Concerns/UsesGeneratorOverrides.php: -------------------------------------------------------------------------------- 1 | rootNamespace())) { 23 | return $model; 24 | } 25 | 26 | return $this->generatorPreset()->modelNamespace().$model; 27 | } 28 | 29 | /** 30 | * Get the destination class path. 31 | */ 32 | protected function getPathUsingCanvas(string $name): string 33 | { 34 | $name = Str::replaceFirst($this->rootNamespace(), '', $name); 35 | 36 | return join_paths($this->getGeneratorSourcePath(), str_replace('\\', '/', $name).'.php'); 37 | } 38 | 39 | /** 40 | * Get the root namespace for the class. 41 | */ 42 | protected function rootNamespaceUsingCanvas(): string 43 | { 44 | return $this->generatorPreset()->rootNamespace(); 45 | } 46 | 47 | /** 48 | * Get the model for the default guard's user provider. 49 | */ 50 | protected function userProviderModelUsingCanvas(?string $guard = null): ?string 51 | { 52 | return $this->generatorPreset()->userProviderModel($guard); 53 | } 54 | 55 | /** 56 | * Get the first view directory path from the application configuration. 57 | */ 58 | protected function viewPathUsingCanvas(string $path = ''): string 59 | { 60 | $views = $this->generatorPreset()->viewPath(); 61 | 62 | return join_paths($views, $path); 63 | } 64 | 65 | /** 66 | * Get a list of possible model names. 67 | * 68 | * @return array 69 | */ 70 | protected function possibleModelsUsingCanvas(): array 71 | { 72 | $sourcePath = $this->generatorPreset()->sourcePath(); 73 | 74 | $modelPath = is_dir(join_paths($sourcePath, 'Models')) ? join_paths($sourcePath, 'Models') : $sourcePath; 75 | 76 | return collect((new Finder)->files()->depth(0)->in($modelPath)) 77 | ->map(fn ($file) => $file->getBasename('.php')) 78 | ->sort() 79 | ->values() 80 | ->all(); 81 | } 82 | 83 | /** 84 | * Get a list of possible event names. 85 | * 86 | * @return array 87 | */ 88 | protected function possibleEventsUsingCanvas(): array 89 | { 90 | $eventPath = join_paths($this->generatorPreset()->sourcePath(), 'Events'); 91 | 92 | if (! is_dir($eventPath)) { 93 | return []; 94 | } 95 | 96 | return collect((new Finder)->files()->depth(0)->in($eventPath)) 97 | ->map(fn ($file) => $file->getBasename('.php')) 98 | ->sort() 99 | ->values() 100 | ->all(); 101 | } 102 | 103 | /** 104 | * Get the root namespace for the class. 105 | * 106 | * @return string 107 | */ 108 | abstract protected function rootNamespace(); 109 | 110 | /** 111 | * Resolve the generator preset. 112 | */ 113 | abstract protected function generatorPreset(): Preset; 114 | } 115 | -------------------------------------------------------------------------------- /src/Contracts/GeneratesCode.php: -------------------------------------------------------------------------------- 1 | app->singleton(PresetManager::class, static fn ($app) => new PresetManager($app)); 16 | } 17 | 18 | /** 19 | * Get the services provided by the provider. 20 | * 21 | * @return array 22 | */ 23 | public function provides(): array 24 | { 25 | return [ 26 | PresetManager::class, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PresetManager.php: -------------------------------------------------------------------------------- 1 | container); 21 | } 22 | 23 | /** 24 | * Set the default driver name. 25 | * 26 | * @param string $name 27 | * @return void 28 | */ 29 | public function setDefaultDriver($name) 30 | { 31 | $this->defaultPreset = $name; 32 | } 33 | 34 | /** 35 | * Get the default driver name. 36 | * 37 | * @return string 38 | */ 39 | public function getDefaultDriver() 40 | { 41 | return $this->defaultPreset; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Presets/Laravel.php: -------------------------------------------------------------------------------- 1 | app->basePath(); 23 | } 24 | 25 | /** 26 | * Get the path to the source directory. 27 | */ 28 | public function sourcePath(): string 29 | { 30 | return $this->app->basePath('app'); 31 | } 32 | 33 | /** 34 | * Get the path to the testing directory. 35 | */ 36 | public function testingPath(): string 37 | { 38 | return $this->app->basePath('tests'); 39 | } 40 | 41 | /** 42 | * Get the path to the resource directory. 43 | */ 44 | public function resourcePath(): string 45 | { 46 | return $this->app->resourcePath(); 47 | } 48 | 49 | /** 50 | * Get the path to the view directory. 51 | */ 52 | public function viewPath(): string 53 | { 54 | return $this->app->make('config')->get('view.paths')[0] ?? $this->app->resourcePath('views'); 55 | } 56 | 57 | /** 58 | * Get the path to the factory directory. 59 | */ 60 | public function factoryPath(): string 61 | { 62 | return $this->app->databasePath('factories'); 63 | } 64 | 65 | /** 66 | * Get the path to the migration directory. 67 | */ 68 | public function migrationPath(): string 69 | { 70 | return $this->app->databasePath('migrations'); 71 | } 72 | 73 | /** 74 | * Get the path to the seeder directory. 75 | */ 76 | public function seederPath(): string 77 | { 78 | if (is_dir($seederPath = $this->app->databasePath('seeds'))) { 79 | return $seederPath; 80 | } 81 | 82 | return $this->app->databasePath('seeders'); 83 | } 84 | 85 | /** 86 | * Preset namespace. 87 | */ 88 | public function rootNamespace(): string 89 | { 90 | return $this->app->getNamespace(); 91 | } 92 | 93 | /** 94 | * Command namespace. 95 | */ 96 | public function commandNamespace(): string 97 | { 98 | return "{$this->rootNamespace()}Console\Commands\\"; 99 | } 100 | 101 | /** 102 | * Model namespace. 103 | */ 104 | public function modelNamespace(): string 105 | { 106 | return is_dir(join_paths($this->sourcePath(), 'Models')) ? "{$this->rootNamespace()}Models\\" : $this->rootNamespace(); 107 | } 108 | 109 | /** 110 | * Provider namespace. 111 | */ 112 | public function providerNamespace(): string 113 | { 114 | return "{$this->rootNamespace()}Providers\\"; 115 | } 116 | 117 | /** 118 | * Testing namespace. 119 | */ 120 | public function testingNamespace(): string 121 | { 122 | return 'Tests\\'; 123 | } 124 | 125 | /** 126 | * Database factory namespace. 127 | */ 128 | public function factoryNamespace(): string 129 | { 130 | return 'Database\Factories\\'; 131 | } 132 | 133 | /** 134 | * Database seeder namespace. 135 | */ 136 | public function seederNamespace(): string 137 | { 138 | return 'Database\Seeders\\'; 139 | } 140 | 141 | /** 142 | * Preset has custom stub path. 143 | */ 144 | public function hasCustomStubPath(): bool 145 | { 146 | return true; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Presets/Preset.php: -------------------------------------------------------------------------------- 1 | name() === $name; 25 | } 26 | 27 | /** 28 | * Get the model for the default guard's user provider. 29 | * 30 | * @return class-string|null 31 | * 32 | * @throws \LogicException 33 | */ 34 | public function userProviderModel(?string $guard = null): ?string 35 | { 36 | /** @var \Illuminate\Contracts\Config\Repository $config */ 37 | $config = $this->app->make('config'); 38 | 39 | $guard = $guard ?: $config->get('auth.defaults.guard'); 40 | 41 | if (\is_null($provider = $config->get("auth.guards.{$guard}.provider"))) { 42 | throw new LogicException(\sprintf('The [%s] guard is not defined in your "auth" configuration file.', $guard)); 43 | } 44 | 45 | return $config->get("auth.providers.{$provider}.model"); 46 | } 47 | 48 | /** 49 | * Preset name. 50 | */ 51 | abstract public function name(): string; 52 | 53 | /** 54 | * Get the path to the base working directory. 55 | */ 56 | abstract public function basePath(): string; 57 | 58 | /** 59 | * Get the path to the source directory. 60 | */ 61 | abstract public function sourcePath(): string; 62 | 63 | /** 64 | * Get the path to the testing directory. 65 | */ 66 | abstract public function testingPath(): string; 67 | 68 | /** 69 | * Get the path to the resource directory. 70 | */ 71 | abstract public function resourcePath(): string; 72 | 73 | /** 74 | * Get the path to the view directory. 75 | */ 76 | abstract public function viewPath(): string; 77 | 78 | /** 79 | * Get the path to the factory directory. 80 | */ 81 | abstract public function factoryPath(): string; 82 | 83 | /** 84 | * Get the path to the migration directory. 85 | */ 86 | abstract public function migrationPath(): string; 87 | 88 | /** 89 | * Get the path to the seeder directory. 90 | */ 91 | abstract public function seederPath(): string; 92 | 93 | /** 94 | * Preset namespace. 95 | */ 96 | abstract public function rootNamespace(): string; 97 | 98 | /** 99 | * Command namespace. 100 | */ 101 | abstract public function commandNamespace(): string; 102 | 103 | /** 104 | * Model namespace. 105 | */ 106 | abstract public function modelNamespace(): string; 107 | 108 | /** 109 | * Provider namespace. 110 | */ 111 | abstract public function providerNamespace(): string; 112 | 113 | /** 114 | * Testing namespace. 115 | */ 116 | abstract public function testingNamespace(): string; 117 | 118 | /** 119 | * Database factory namespace. 120 | */ 121 | abstract public function factoryNamespace(): string; 122 | 123 | /** 124 | * Database seeder namespace. 125 | */ 126 | abstract public function seederNamespace(): string; 127 | 128 | /** 129 | * Preset has custom stub path. 130 | */ 131 | abstract public function hasCustomStubPath(): bool; 132 | } 133 | --------------------------------------------------------------------------------