Skip to content

Commit 1ce5c57

Browse files
committed
wip add scaffolding system
1 parent 11cc24d commit 1ce5c57

File tree

50 files changed

+2793
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2793
-1
lines changed

src/Maker/MakeScaffold.php

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Maker;
13+
14+
use Composer\InstalledVersions;
15+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
16+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
17+
use Symfony\Bundle\MakerBundle\Generator;
18+
use Symfony\Bundle\MakerBundle\InputConfiguration;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Input\InputArgument;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Filesystem\Filesystem;
23+
use Symfony\Component\Finder\Finder;
24+
use Symfony\Component\HttpKernel\Kernel;
25+
use Symfony\Component\Process\Process;
26+
27+
/**
28+
* @author Kevin Bond <[email protected]>
29+
*/
30+
final class MakeScaffold extends AbstractMaker
31+
{
32+
private $projectDir;
33+
private $availableScaffolds;
34+
private $installedScaffolds = [];
35+
private $installedPackages = [];
36+
37+
public function __construct($projectDir)
38+
{
39+
$this->projectDir = $projectDir;
40+
}
41+
42+
public static function getCommandName(): string
43+
{
44+
return 'make:scaffold';
45+
}
46+
47+
public static function getCommandDescription(): string
48+
{
49+
return 'Create scaffold'; // todo
50+
}
51+
52+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
53+
{
54+
$command
55+
->addArgument('name', InputArgument::OPTIONAL|InputArgument::IS_ARRAY, 'Scaffold name(s) to create')
56+
;
57+
58+
$inputConfig->setArgumentAsNonInteractive('name');
59+
}
60+
61+
public function configureDependencies(DependencyBuilder $dependencies): void
62+
{
63+
$dependencies->addClassDependency(Process::class, 'process');
64+
}
65+
66+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
67+
{
68+
$names = $input->getArgument('name');
69+
70+
if (!$names) {
71+
throw new \InvalidArgumentException('You must select at least one scaffold.');
72+
}
73+
74+
foreach ($names as $name) {
75+
$this->generateScaffold($name, $io);
76+
}
77+
}
78+
79+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
80+
{
81+
if ($input->getArgument('name')) {
82+
return;
83+
}
84+
85+
$availableScaffolds = array_combine(
86+
array_keys($this->availableScaffolds()),
87+
array_map(fn(array $scaffold) => $scaffold['description'], $this->availableScaffolds())
88+
);
89+
90+
$input->setArgument('name', [$io->choice('Available scaffolds', $availableScaffolds)]);
91+
}
92+
93+
private function generateScaffold(string $name, ConsoleStyle $io): void
94+
{
95+
if ($this->isScaffoldInstalled($name)) {
96+
return;
97+
}
98+
99+
if (!isset($this->availableScaffolds()[$name])) {
100+
throw new \InvalidArgumentException("Scaffold \"{$name}\" does not exist for your version of Symfony.");
101+
}
102+
103+
$scaffold = $this->availableScaffolds()[$name];
104+
105+
// install dependent scaffolds
106+
foreach ($scaffold['dependents'] ?? [] as $dependent) {
107+
$this->generateScaffold($dependent, $io);
108+
}
109+
110+
$io->text("Generating <info>{$name}</info> Scaffold...");
111+
112+
// install required packages
113+
foreach ($scaffold['packages'] ?? [] as $package => $env) {
114+
if (!$this->isPackageInstalled($package)) {
115+
$io->text("Installing <comment>{$package}</comment>...");
116+
117+
// todo composer bin detection
118+
$command = ['composer', 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package];
119+
$process = new Process(array_filter($command), $this->projectDir);
120+
121+
$process->run();
122+
123+
if (!$process->isSuccessful()) {
124+
throw new \RuntimeException("Error installing \"{$package}\".");
125+
}
126+
127+
$this->installedPackages[] = $package;
128+
}
129+
}
130+
131+
$io->text('Copying scaffold files...');
132+
133+
(new Filesystem())->mirror($scaffold['dir'], $this->projectDir, null, ['override' => true]);
134+
135+
$io->text("Successfully installed scaffold <info>{$name}</info>.");
136+
$io->newLine();
137+
138+
$this->installedScaffolds[] = $name;
139+
}
140+
141+
private function availableScaffolds(): array
142+
{
143+
if (is_array($this->availableScaffolds)) {
144+
return $this->availableScaffolds;
145+
}
146+
147+
$this->availableScaffolds = [];
148+
$finder = Finder::create()
149+
// todo, improve versioning system
150+
->in(\sprintf('%s/../Resources/scaffolds/%s.0', __DIR__, Kernel::MAJOR_VERSION))
151+
->name('*.json')
152+
;
153+
154+
foreach ($finder as $file) {
155+
$name = $file->getFilenameWithoutExtension();
156+
157+
$this->availableScaffolds[$name] = array_merge(
158+
json_decode(file_get_contents($file), true),
159+
['dir' => dirname($file->getRealPath()).'/'.$name]
160+
);
161+
}
162+
163+
return $this->availableScaffolds;
164+
}
165+
166+
/**
167+
* Detect if package already installed or installed in this process
168+
* (when installing multiple scaffolds at once).
169+
*/
170+
private function isPackageInstalled(string $package): bool
171+
{
172+
return InstalledVersions::isInstalled($package) || \in_array($package, $this->installedPackages, true);
173+
}
174+
175+
/**
176+
* Detect if package is installed in the same process (when installing
177+
* multiple scaffolds at once).
178+
*/
179+
private function isScaffoldInstalled(string $name): bool
180+
{
181+
return \in_array($name, $this->installedScaffolds, true);
182+
}
183+
}

src/Resources/config/makers.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
<tag name="maker.command" />
2626
</service>
2727

28+
<service id="maker.maker.make_scaffold" class="Symfony\Bundle\MakerBundle\Maker\MakeScaffold">
29+
<argument>%kernel.project_dir%</argument>
30+
<tag name="maker.command" />
31+
</service>
32+
2833
<service id="maker.maker.make_crud" class="Symfony\Bundle\MakerBundle\Maker\MakeCrud">
2934
<argument type="service" id="maker.doctrine_helper" />
3035
<argument type="service" id="maker.renderer.form_type_renderer" />

src/Resources/scaffolds/6.0/auth.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"description": "Create login form and tests.",
3+
"dependents": [
4+
"homepage",
5+
"user"
6+
],
7+
"packages": {
8+
"profiler": "dev"
9+
}
10+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
security:
2+
enable_authenticator_manager: true
3+
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
4+
password_hashers:
5+
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
6+
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
7+
providers:
8+
# used to reload user from session & other features (e.g. switch_user)
9+
app_user_provider:
10+
entity:
11+
class: App\Entity\User
12+
property: email
13+
firewalls:
14+
dev:
15+
pattern: ^/(_(profiler|wdt)|css|images|js)/
16+
security: false
17+
main:
18+
lazy: true
19+
provider: app_user_provider
20+
form_login:
21+
login_path: login
22+
check_path: login
23+
username_parameter: email
24+
password_parameter: password
25+
enable_csrf: true
26+
logout:
27+
path: logout
28+
# where to redirect after logout
29+
target: homepage
30+
remember_me:
31+
secret: '%kernel.secret%'
32+
secure: auto
33+
samesite: lax
34+
35+
# activate different ways to authenticate
36+
# https://symfony.com/doc/current/security.html#the-firewall
37+
38+
# https://symfony.com/doc/current/security/impersonating_user.html
39+
# switch_user: true
40+
41+
# Easy way to control access for large sections of your site
42+
# Note: Only the *first* access control that matches will be used
43+
access_control:
44+
# - { path: ^/admin, roles: ROLE_ADMIN }
45+
# - { path: ^/profile, roles: ROLE_USER }
46+
47+
when@test:
48+
security:
49+
password_hashers:
50+
# By default, password hashers are resource intensive and take time. This is
51+
# important to generate secure password hashes. In tests however, secure hashes
52+
# are not important, waste resources and increase test times. The following
53+
# reduces the work factor to the lowest possible values.
54+
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
55+
algorithm: auto
56+
cost: 4 # Lowest possible value for bcrypt
57+
time_cost: 3 # Lowest possible value for argon
58+
memory_cost: 10 # Lowest possible value for argon
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# This file is the entry point to configure your own services.
2+
# Files in the packages/ subdirectory configure your dependencies.
3+
4+
# Put parameters here that don't need to change on each machine where the app is deployed
5+
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
6+
parameters:
7+
8+
services:
9+
# default configuration for services in *this* file
10+
_defaults:
11+
autowire: true # Automatically injects dependencies in your services.
12+
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
13+
14+
# makes classes in src/ available to be used as services
15+
# this creates a service per class whose id is the fully-qualified class name
16+
App\:
17+
resource: '../src/'
18+
exclude:
19+
- '../src/DependencyInjection/'
20+
- '../src/Entity/'
21+
- '../src/Kernel.php'
22+
23+
# add more service definitions when explicit configuration is needed
24+
# please note that last definitions always *replace* previous ones
25+
26+
# programmatic user login service
27+
App\Security\LoginUser:
28+
$authenticator: '@security.authenticator.form_login.main'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\RedirectResponse;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\Routing\Annotation\Route;
10+
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
11+
12+
class SecurityController extends AbstractController
13+
{
14+
#[Route(path: '/login', name: 'login')]
15+
public function login(AuthenticationUtils $authenticationUtils, Request $request): Response
16+
{
17+
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
18+
return new RedirectResponse($request->query->get('target', $this->generateUrl('homepage')));
19+
}
20+
21+
// get the login error if there is one
22+
$error = $authenticationUtils->getLastAuthenticationError();
23+
24+
// current user's username (if already logged in) or last username entered by the user
25+
$lastUsername = $this->getUser()?->getUserIdentifier() ?? $authenticationUtils->getLastUsername();
26+
27+
return $this->render('login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
28+
}
29+
30+
#[Route(path: '/logout', name: 'logout')]
31+
public function logout(): void
32+
{
33+
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
34+
}
35+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace App\Security;
4+
5+
use App\Entity\User;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
8+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
9+
10+
class LoginUser
11+
{
12+
public function __construct(
13+
private UserAuthenticatorInterface $userAuthenticator,
14+
private AuthenticatorInterface $authenticator,
15+
) {
16+
}
17+
18+
public function __invoke(User $user, Request $request): void
19+
{
20+
$this->userAuthenticator->authenticateUser($user, $this->authenticator, $request);
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% extends 'base.html.twig' %}
2+
3+
{% block title %}Log in!{% endblock %}
4+
5+
{% block body %}
6+
<form method="post">
7+
{% if error %}
8+
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
9+
{% endif %}
10+
11+
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
12+
<label for="inputEmail">Email</label>
13+
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
14+
<label for="inputPassword">Password</label>
15+
<input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>
16+
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
17+
<div class="checkbox mb-3">
18+
<label>
19+
<input type="checkbox" checked name="_remember_me"> Remember me
20+
</label>
21+
</div>
22+
23+
{% if app.request.query.has('target') %}
24+
<input type="hidden" name="_target_path" value="{{ app.request.query.get('target') }}"/>
25+
{% endif %}
26+
27+
<button class="btn btn-lg btn-primary" type="submit">
28+
Sign in
29+
</button>
30+
</form>
31+
{% endblock %}

0 commit comments

Comments
 (0)