- Basic HTML templates

- Created entities & new migration
- Added packages symfony/stimulus-bundle & symfony/webpack-encore-bundle
This commit is contained in:
Skylar Sadlier 2023-10-14 02:11:28 -06:00
parent e8df5bf019
commit 3ed53ac05e
33 changed files with 9009 additions and 42 deletions

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

8
.gitignore vendored
View File

@ -20,4 +20,10 @@
###< symfony/phpunit-bridge ### ###< symfony/phpunit-bridge ###
# IDE folders # IDE folders
.idea .idea
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

28
assets/app.js Normal file
View File

@ -0,0 +1,28 @@
import './bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';
require('fontawesome-free/js/all.js');
import * as mdb from 'mdb-ui-kit'; // lib
import { Input } from 'mdb-ui-kit'; // module
const $ = require('jquery');
// this "modifies" the jquery module: adding behavior to it
// the bootstrap module doesn't export/return anything
require('bootstrap');
// or you can include specific pieces
// require('bootstrap/js/dist/tooltip');
// require('bootstrap/js/dist/popover');
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
});

12
assets/bootstrap.js vendored Normal file
View File

@ -0,0 +1,12 @@
// assets/bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.(j|t)sx?$/
));
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

4
assets/controllers.json Normal file
View File

@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

View File

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

67
assets/styles/app.scss Normal file
View File

@ -0,0 +1,67 @@
body {
background-color: lightgray;
}
// customize some Bootstrap variables
$primary: darken(#428bca, 20%);
// the ~ allows you to reference things in node_modules
@import "~bootstrap/scss/bootstrap";
@import '~mdb-ui-kit/css/mdb.min.css';
// importing core styling file
@import "~fontawesome-free/scss/fontawesome.scss";
// our project needs Classic Solid, Brands, and Sharp Solid
@import "~fontawesome-free/scss/solid.scss";
@import "~fontawesome-free/scss/brands.scss";
@import "~fontawesome-free/scss/regular.scss";
/* for login page */
.gradient-custom {
/* fallback for old browsers */
background: #6a11cb;
/* Chrome 10-25, Safari 5.1-6 */
background: -webkit-linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1));
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
}
@media (min-width: 991.98px) {
main {
padding-left: 240px;
}
}
/* Sidebar */
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
padding: 58px 0 0; /* Height of navbar */
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 5%), 0 2px 10px 0 rgb(0 0 0 / 5%);
width: 240px;
z-index: 600;
}
@media (max-width: 991.98px) {
.sidebar {
width: 100%;
}
}
.sidebar .active {
border-radius: 5px;
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 16%), 0 2px 10px 0 rgb(0 0 0 / 12%);
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: 0.5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}

View File

@ -7,6 +7,7 @@
"php": ">=8.1", "php": ">=8.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-intl": "*",
"doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.16", "doctrine/orm": "^2.16",
@ -35,11 +36,13 @@
"symfony/runtime": "6.3.*", "symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*", "symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*", "symfony/serializer": "6.3.*",
"symfony/stimulus-bundle": "^2.11",
"symfony/string": "6.3.*", "symfony/string": "6.3.*",
"symfony/translation": "6.3.*", "symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*", "symfony/twig-bundle": "6.3.*",
"symfony/validator": "6.3.*", "symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*", "symfony/web-link": "6.3.*",
"symfony/webpack-encore-bundle": "^2.0",
"symfony/yaml": "6.3.*", "symfony/yaml": "6.3.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"

144
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "0e74667a94096c60a3ca270a79c1331d", "content-hash": "f0b903562ace7dd636451d35de6a7255",
"packages": [ "packages": [
{ {
"name": "behat/transliterator", "name": "behat/transliterator",
@ -6499,6 +6499,74 @@
], ],
"time": "2023-05-23T14:45:45+00:00" "time": "2023-05-23T14:45:45+00:00"
}, },
{
"name": "symfony/stimulus-bundle",
"version": "v2.11.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/stimulus-bundle.git",
"reference": "e0e19de8df4d5b2bed57328ae69ef7904df660c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/e0e19de8df4d5b2bed57328ae69ef7904df660c7",
"reference": "e0e19de8df4d5b2bed57328ae69ef7904df660c7",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/finder": "^5.4|^6.0",
"symfony/http-kernel": "^5.4|^6.0",
"twig/twig": "^2.15.3|^3.4.3"
},
"require-dev": {
"symfony/asset-mapper": "^6.3",
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/phpunit-bridge": "^5.4|^6.0",
"symfony/twig-bundle": "^5.4|^6.0",
"zenstruck/browser": "^1.4"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Symfony\\UX\\StimulusBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Stimulus!",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.11.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-08-28T18:00:50+00:00"
},
{ {
"name": "symfony/stopwatch", "name": "symfony/stopwatch",
"version": "v6.3.0", "version": "v6.3.0",
@ -7350,6 +7418,77 @@
], ],
"time": "2023-04-21T14:41:17+00:00" "time": "2023-04-21T14:41:17+00:00"
}, },
{
"name": "symfony/webpack-encore-bundle",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/webpack-encore-bundle.git",
"reference": "150fe022740fef908f4ca3d5950ce85ab040ec76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/150fe022740fef908f4ca3d5950ce85ab040ec76",
"reference": "150fe022740fef908f4ca3d5950ce85ab040ec76",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/asset": "^5.4 || ^6.2",
"symfony/config": "^5.4 || ^6.2",
"symfony/dependency-injection": "^5.4 || ^6.2",
"symfony/http-kernel": "^5.4 || ^6.2",
"symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
},
"require-dev": {
"symfony/framework-bundle": "^5.4 || ^6.2",
"symfony/phpunit-bridge": "^5.4 || ^6.2",
"symfony/twig-bundle": "^5.4 || ^6.2",
"symfony/web-link": "^5.4 || ^6.2"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/webpack-encore",
"url": "https://github.com/symfony/webpack-encore"
}
},
"autoload": {
"psr-4": {
"Symfony\\WebpackEncoreBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Webpack Encore!",
"support": {
"issues": "https://github.com/symfony/webpack-encore-bundle/issues",
"source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.0.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-05-31T14:28:33+00:00"
},
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v6.3.3", "version": "v6.3.3",
@ -9895,7 +10034,8 @@
"platform": { "platform": {
"php": ">=8.1", "php": ">=8.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*" "ext-iconv": "*",
"ext-intl": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"

View File

@ -11,4 +11,6 @@ return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
]; ];

View File

@ -1,4 +1,5 @@
twig: twig:
form_themes: ['bootstrap_5_layout.html.twig']
default_path: '%kernel.project_dir%/templates' default_path: '%kernel.project_dir%/templates'
when@test: when@test:

View File

@ -0,0 +1,45 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
#when@prod:
# webpack_encore:
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# # Available in version 1.2
# cache: true
#when@test:
# webpack_encore:
# strict_mode: false

View File

@ -20,5 +20,10 @@ services:
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones
gedmo.listener.timestampable:
class: Gedmo\Timestampable\TimestampableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ '@annotation_reader' ] ]

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230909100148 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD display_name VARCHAR(64) NOT NULL, ADD created_at DATETIME NOT NULL, ADD updated_at DATETIME DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE `user` DROP display_name, DROP created_at, DROP updated_at');
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231014080536 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, page_id INT NOT NULL, user_id INT DEFAULT NULL, parent_comment_id INT DEFAULT NULL, deleted_by_user_id INT DEFAULT NULL, markdown LONGTEXT NOT NULL, html LONGTEXT NOT NULL, score INT NOT NULL, state VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL, deleted_at DATETIME DEFAULT NULL, INDEX IDX_9474526CC4663E4 (page_id), INDEX IDX_9474526CA76ED395 (user_id), INDEX IDX_9474526CBF2AF943 (parent_comment_id), INDEX IDX_9474526CFCF2A97A (deleted_by_user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE domain_page (id INT AUTO_INCREMENT NOT NULL, domain_id INT NOT NULL, path LONGTEXT NOT NULL, locked TINYINT(1) NOT NULL, comment_count INT NOT NULL, title LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL, deleted_at DATETIME DEFAULT NULL, INDEX IDX_B9D69B47115F0EE5 (domain_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE page_view (id INT AUTO_INCREMENT NOT NULL, page_id INT NOT NULL, user_id INT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_7939B754C4663E4 (page_id), INDEX IDX_7939B754A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CC4663E4 FOREIGN KEY (page_id) REFERENCES domain_page (id)');
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CBF2AF943 FOREIGN KEY (parent_comment_id) REFERENCES comment (id)');
$this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CFCF2A97A FOREIGN KEY (deleted_by_user_id) REFERENCES `user` (id)');
$this->addSql('ALTER TABLE domain_page ADD CONSTRAINT FK_B9D69B47115F0EE5 FOREIGN KEY (domain_id) REFERENCES domain (id)');
$this->addSql('ALTER TABLE page_view ADD CONSTRAINT FK_7939B754C4663E4 FOREIGN KEY (page_id) REFERENCES domain_page (id)');
$this->addSql('ALTER TABLE page_view ADD CONSTRAINT FK_7939B754A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CC4663E4');
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CA76ED395');
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CBF2AF943');
$this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CFCF2A97A');
$this->addSql('ALTER TABLE domain_page DROP FOREIGN KEY FK_B9D69B47115F0EE5');
$this->addSql('ALTER TABLE page_view DROP FOREIGN KEY FK_7939B754C4663E4');
$this->addSql('ALTER TABLE page_view DROP FOREIGN KEY FK_7939B754A76ED395');
$this->addSql('DROP TABLE comment');
$this->addSql('DROP TABLE domain_page');
$this->addSql('DROP TABLE page_view');
}
}

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "MyComments",
"packageManager": "yarn@3.6.3",
"devDependencies": {
"@babel/core": "^7.22.17",
"@babel/preset-env": "^7.22.15",
"@hotwired/stimulus": "^3.0.0",
"@popperjs/core": "^2.11.8",
"@symfony/webpack-encore": "^4.4.0",
"babel-loader": "^9.1.3",
"bootstrap": "^5.3.1",
"core-js": "3.32.2",
"jquery": "^3.7.1",
"sass": "^1.66.1",
"sass-loader": "^13.0.0",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-notifier": "^1.15.0"
},
"dependencies": {
"@symfony/stimulus-bridge": "^3.2.2",
"fontawesome-free": "^1.0.4",
"mdb-ui-kit": "^6.4.1"
},
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
}

View File

@ -0,0 +1,262 @@
<?php
namespace App\Command;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use function Symfony\Component\String\u;
/**
* A console command that creates users and stores them in the database.
*
* To use this command, open a terminal window, enter into your project
* directory and execute the following:
*
* $ php bin/console app:add-user
*
* To output detailed information, increase the command verbosity:
*
* $ php bin/console app:add-user -vv
*
* See https://symfony.com/doc/current/console.html
*
* We use the default services.yaml configuration, so command classes are registered as services.
* See https://symfony.com/doc/current/console/commands_as_services.html
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
#[AsCommand(
name: 'app:add-user',
description: 'Creates users and stores them in the database'
)]
final class AddUserCommand extends Command
{
private SymfonyStyle $io;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $passwordHasher,
private readonly ValidatorInterface $validator,
private readonly UserRepository $users
) {
parent::__construct();
}
protected function configure(): void
{
$this
->setHelp($this->getCommandHelp())
// commands can optionally define arguments and/or options (mandatory and optional)
// see https://symfony.com/doc/current/components/console/console_arguments.html
->addArgument('display-name', InputArgument::OPTIONAL, 'The display name of the new user')
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
->addOption('owner', null, InputOption::VALUE_NONE, 'If set, the user is created as an owner')
;
}
/**
* This optional method is the first one executed for a command after configure()
* and is useful to initialize properties based on the input arguments and options.
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// SymfonyStyle is an optional feature that Symfony provides so you can
// apply a consistent look to the commands of your application.
// See https://symfony.com/doc/current/console/style.html
$this->io = new SymfonyStyle($input, $output);
}
/**
* This method is executed after initialize() and before execute(). Its purpose
* is to check if some of the options/arguments are missing and interactively
* ask the user for those values.
*
* This method is completely optional. If you are developing an internal console
* command, you probably should not implement this method because it requires
* quite a lot of work. However, if the command is meant to be used by external
* users, this method is a nice way to fall back and prevent errors.
*/
protected function interact(InputInterface $input, OutputInterface $output): void
{
if (null !== $input->getArgument('password') && null !== $input->getArgument('email') && null !== $input->getArgument('display-name')) {
return;
}
$this->io->title('Add User Command Interactive Wizard');
$this->io->text([
'If you prefer to not use this interactive wizard, provide the',
'arguments required by this command as follows:',
'',
' $ php bin/console app:add-user display-name password email@example.com',
'',
'Now we\'ll ask you for the value of all the missing command arguments.',
]);
// Ask for the display name if it's not defined
$displayName = $input->getArgument('display-name');
if (null !== $displayName) {
$this->io->text(' > <info>Display Name</info>: '.$displayName);
} else {
$displayName = $this->io->ask('display Name', null, function($answer){
$validation = $this->validator->validatePropertyValue(User::class, 'displayName', $answer);
if($validation->count()) {
foreach($validation as $validationError) {
$this->io->error("{$validationError->getMessage()}");
}
throw new Exception("Invalid display name");
}
});
$input->setArgument('display-name', $displayName);
}
// Ask for the password if it's not defined
/** @var string|null $password */
$password = $input->getArgument('password');
if (null !== $password) {
$this->io->text(' > <info>Password</info>: '.u('*')->repeat(u($password)->length()));
} else {
$password = $this->io->askHidden('Password (your type will be hidden)', function($answer){
// from the CLI we don't really want to impose too many restrictions on the password
if (empty($plainPassword)) {
throw new InvalidArgumentException('The password can not be empty.');
}
if (u($plainPassword)->trim()->length() < 6) {
throw new InvalidArgumentException('The password must be at least 6 characters long.');
}
return $answer;
});
$input->setArgument('password', $password);
}
// Ask for the email if it's not defined
$email = $input->getArgument('email');
if (null !== $email) {
$this->io->text(' > <info>Email</info>: '.$email);
} else {
$email = $this->io->ask('Email', null, $this->validator->validateEmail(...));
$input->setArgument('email', $email);
}
}
/**
* This method is executed after interact() and initialize(). It usually
* contains the logic to execute to complete this command task.
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$stopwatch = new Stopwatch();
$stopwatch->start('add-user-command');
/** @var string $plainPassword */
$plainPassword = $input->getArgument('password');
/** @var string $email */
$email = $input->getArgument('email');
/** @var string $displayName */
$displayName = $input->getArgument('display-name');
$isAdmin = $input->getOption('admin');
$isOwner = $input->getOption('owner');
// make sure to validate the user data is correct
$this->validateUserData($plainPassword, $email, $displayName);
// create the user and hash its password
$user = new User();
$user->setDisplayName($displayName)
->setEmail($email)
->setRoles([$isAdmin ? User::ROLE_ADMIN : User::ROLE_USER])
->addRole(User::ROLE_USER);
if($isAdmin) {
$user->addRole(User::ROLE_ADMIN);
}
if($isOwner) {
$user->addRole(User::ROLE_OWNER);
}
// See https://symfony.com/doc/5.4/security.html#registering-the-user-hashing-passwords
$hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword);
$user->setPassword($hashedPassword);
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getDisplayName(), $user->getEmail()));
$event = $stopwatch->stop('add-user-command');
if ($output->isVerbose()) {
$this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2)));
}
return Command::SUCCESS;
}
private function validateUserData(string $plainPassword, string $email, string $displayName): void
{
// validate password and email if is not this input means interactive.
$this->validator->validatePassword($plainPassword);
$this->validator->validateEmail($email);
$this->validator->validateDisplayName($displayName);
// check if a user with the same email already exists.
$existingEmail = $this->users->findOneBy(['email' => $email]);
if (null !== $existingEmail) {
throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email));
}
}
/**
* The command help is usually included in the configure() method, but when
* it's too long, it's better to define a separate method to maintain the
* code readability.
*/
private function getCommandHelp(): string
{
return <<<'HELP'
The <info>%command.name%</info> command creates new users and saves them in the database:
<info>php %command.full_name%</info> <comment>display-name password email</comment>
By default the command creates regular users. To create administrator users,
add the <comment>--admin</comment> option:
<info>php %command.full_name%</info> display-name password email <comment>--admin</comment>
If you omit any of the three required arguments, the command will ask you to
provide the missing values:
# command will ask you for the email
<info>php %command.full_name%</info> <comment>display-name password</comment>
# command will ask you for the email and password
<info>php %command.full_name%</info> <comment>display-name</comment>
# command will ask you for all arguments
<info>php %command.full_name%</info>
HELP;
}
}

View File

@ -13,6 +13,11 @@ class SecurityController extends AbstractController
#[Route('/login', name: 'app_login')] #[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {
// redirect already logged users to the dashboard
if($this->getUser()) {
return $this->redirectToRoute('app_dashboard');
}
// get the login error if there is one // get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
@ -29,6 +34,9 @@ class SecurityController extends AbstractController
#[Route('/logout', name: 'app_logout', methods: ['GET'])] #[Route('/logout', name: 'app_logout', methods: ['GET'])]
public function logout(Security $security): Response public function logout(Security $security): Response
{ {
return $security->logout(false); if($this->getUser()) {
$security->logout(false);
}
return $this->redirectToRoute('app_login');
} }
} }

229
src/Entity/Comment.php Normal file
View File

@ -0,0 +1,229 @@
<?php
namespace App\Entity;
use App\Repository\CommentRepository;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
class Comment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)]
private ?DomainPage $page = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
private ?User $user = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $markdown = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $html = null;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'childComments')]
private ?self $parentComment = null;
#[ORM\OneToMany(mappedBy: 'parentComment', targetEntity: self::class)]
private Collection $childComments;
#[ORM\Column]
private ?int $score = null;
#[ORM\Column(length: 32)]
private ?string $state = null;
#[ORM\Column]
private ?DateTimeImmutable $createdAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?DateTimeInterface $updatedAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?DateTimeInterface $deletedAt = null;
#[ORM\ManyToOne]
private ?User $deletedByUser = null;
public function __construct()
{
$this->childComments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getMarkdown(): ?string
{
return $this->markdown;
}
public function setMarkdown(string $markdown): static
{
$this->markdown = $markdown;
return $this;
}
public function getHtml(): ?string
{
return $this->html;
}
public function setHtml(string $html): static
{
$this->html = $html;
return $this;
}
public function getParentComment(): ?self
{
return $this->parentComment;
}
public function setParentComment(?self $parentComment): static
{
$this->parentComment = $parentComment;
return $this;
}
/**
* @return Collection<int, self>
*/
public function getChildComments(): Collection
{
return $this->childComments;
}
public function addChildComment(self $childComment): static
{
if (!$this->childComments->contains($childComment)) {
$this->childComments->add($childComment);
$childComment->setParentComment($this);
}
return $this;
}
public function removeChildComment(self $childComment): static
{
if ($this->childComments->removeElement($childComment)) {
// set the owning side to null (unless already changed)
if ($childComment->getParentComment() === $this) {
$childComment->setParentComment(null);
}
}
return $this;
}
public function getScore(): ?int
{
return $this->score;
}
public function setScore(int $score): static
{
$this->score = $score;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getDeletedAt(): ?DateTimeInterface
{
return $this->deletedAt;
}
public function setDeletedAt(?DateTimeInterface $deletedAt): static
{
$this->deletedAt = $deletedAt;
return $this;
}
public function getDeletedByUser(): ?User
{
return $this->deletedByUser;
}
public function setDeletedByUser(?User $deletedByUser): static
{
$this->deletedByUser = $deletedByUser;
return $this;
}
public function getPage(): ?DomainPage
{
return $this->page;
}
public function setPage(?DomainPage $page): static
{
$this->page = $page;
return $this;
}
}

View File

@ -3,6 +3,9 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\DomainRepository; use App\Repository\DomainRepository;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation\Timestampable; use Gedmo\Mapping\Annotation\Timestampable;
@ -32,11 +35,23 @@ class Domain
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
#[Timestampable(on: "update")] #[Timestampable(on: "update")]
private ?\DateTimeInterface $updatedAt = null; private ?DateTimeInterface $updatedAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)] #[ORM\Column(type: Types::DATETIME_MUTABLE)]
#[Timestampable(on: "create")] #[Timestampable(on: "create")]
private ?\DateTimeInterface $createdAt = null; private ?DateTimeInterface $createdAt = null;
#[ORM\OneToMany(mappedBy: 'domain', targetEntity: Comment::class)]
private Collection $comments;
#[ORM\OneToMany(mappedBy: 'domain', targetEntity: DomainPage::class)]
private Collection $domainPages;
public function __construct()
{
$this->comments = new ArrayCollection();
$this->domainPages = new ArrayCollection();
}
public function getId(): ?int public function getId(): ?int
{ {
@ -103,27 +118,87 @@ class Domain
return $this; return $this;
} }
public function getUpdatedAt(): ?\DateTimeInterface public function getUpdatedAt(): ?DateTimeInterface
{ {
return $this->updatedAt; return $this->updatedAt;
} }
public function setUpdatedAt(?\DateTimeInterface $updatedAt): static public function setUpdatedAt(?DateTimeInterface $updatedAt): static
{ {
$this->updatedAt = $updatedAt; $this->updatedAt = $updatedAt;
return $this; return $this;
} }
public function getCreatedAt(): ?\DateTimeInterface public function getCreatedAt(): ?DateTimeInterface
{ {
return $this->createdAt; return $this->createdAt;
} }
public function setCreatedAt(\DateTimeInterface $createdAt): static public function setCreatedAt(DateTimeInterface $createdAt): static
{ {
$this->createdAt = $createdAt; $this->createdAt = $createdAt;
return $this; return $this;
} }
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setDomain($this);
}
return $this;
}
public function removeComment(Comment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getDomain() === $this) {
$comment->setDomain(null);
}
}
return $this;
}
/**
* @return Collection<int, DomainPage>
*/
public function getDomainPages(): Collection
{
return $this->domainPages;
}
public function addDomainPage(DomainPage $domainPage): static
{
if (!$this->domainPages->contains($domainPage)) {
$this->domainPages->add($domainPage);
$domainPage->setDomain($this);
}
return $this;
}
public function removeDomainPage(DomainPage $domainPage): static
{
if ($this->domainPages->removeElement($domainPage)) {
// set the owning side to null (unless already changed)
if ($domainPage->getDomain() === $this) {
$domainPage->setDomain(null);
}
}
return $this;
}
} }

184
src/Entity/DomainPage.php Normal file
View File

@ -0,0 +1,184 @@
<?php
namespace App\Entity;
use App\Repository\DomainPageRepository;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: DomainPageRepository::class)]
class DomainPage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'domainPages')]
#[ORM\JoinColumn(nullable: false)]
private ?Domain $domain = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $path = null;
#[ORM\Column]
private ?bool $locked = null;
#[ORM\Column]
private ?int $commentCount = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $title = null;
#[ORM\Column]
private ?DateTimeImmutable $createdAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?DateTimeInterface $updatedAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?DateTimeInterface $deletedAt = null;
#[ORM\OneToMany(mappedBy: 'page', targetEntity: Comment::class)]
private Collection $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getDomain(): ?Domain
{
return $this->domain;
}
public function setDomain(?Domain $domain): static
{
$this->domain = $domain;
return $this;
}
public function getPath(): ?string
{
return $this->path;
}
public function setPath(string $path): static
{
$this->path = $path;
return $this;
}
public function isLocked(): ?bool
{
return $this->locked;
}
public function setLocked(bool $locked): static
{
$this->locked = $locked;
return $this;
}
public function getCommentCount(): ?int
{
return $this->commentCount;
}
public function setCommentCount(int $commentCount): static
{
$this->commentCount = $commentCount;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getDeletedAt(): ?DateTimeInterface
{
return $this->deletedAt;
}
public function setDeletedAt(?DateTimeInterface $deletedAt): static
{
$this->deletedAt = $deletedAt;
return $this;
}
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setPage($this);
}
return $this;
}
public function removeComment(Comment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getPage() === $this) {
$comment->setPage(null);
}
}
return $this;
}
}

67
src/Entity/PageView.php Normal file
View File

@ -0,0 +1,67 @@
<?php
namespace App\Entity;
use App\Repository\PageViewRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PageViewRepository::class)]
class PageView
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?DomainPage $page = null;
#[ORM\ManyToOne]
private ?User $user = null;
#[ORM\Column]
private ?DateTimeImmutable $createdAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getPage(): ?DomainPage
{
return $this->page;
}
public function setPage(?DomainPage $page): static
{
$this->page = $page;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}

View File

@ -3,22 +3,44 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation\Timestampable;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')] #[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface class User implements UserInterface, PasswordAuthenticatedUserInterface
{ {
const ROLE_USER = 'ROLE_USER';
const ROLE_OWNER = 'ROLE_OWNER';
const ROLE_ADMIN = 'ROLE_ADMIN';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180, unique: true)] #[ORM\Column(length: 180, unique: true)]
#[Assert\NotBlank]
#[Assert\Email(mode: Assert\Email::VALIDATION_MODE_HTML5)]
#[Assert\Length(max: 180)]
#[Assert\NoSuspiciousCharacters()]
private ?string $email = null; private ?string $email = null;
#[ORM\Column(length: 64)]
#[Assert\NotBlank]
#[Assert\Length(max: 64)]
#[Assert\NoSuspiciousCharacters()]
#[Assert\Regex(pattern: "/^[a-zA-Z0-9 _\-]+$/", message: "Display name can only contain characters [a-zA-Z0-9 _-]")]
private ?string $displayName = null;
#[ORM\Column] #[ORM\Column]
private array $roles = []; private array $roles = [];
@ -28,6 +50,22 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column] #[ORM\Column]
private ?string $password = null; private ?string $password = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
#[Timestampable(on: "create")]
private ?DateTimeInterface $createdAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
#[Timestampable(on: "update")]
private ?DateTimeInterface $updatedAt = null;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Comment::class)]
private Collection $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -45,6 +83,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getDisplayName(): ?string
{
return $this->displayName;
}
public function setDisplayName(string $displayName): static
{
$this->displayName = $displayName;
return $this;
}
/** /**
* A visual identifier that represents this user. * A visual identifier that represents this user.
* *
@ -74,6 +124,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function addRole(string $role): static
{
if(!$this->roles) {
$this->roles = [];
}
if(!in_array($role, $this->roles)) {
$this->roles[] = $role;
}
return $this;
}
/** /**
* @see PasswordAuthenticatedUserInterface * @see PasswordAuthenticatedUserInterface
*/ */
@ -97,4 +160,58 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
// If you store any temporary, sensitive data on the user, clear it here // If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null; // $this->plainPassword = null;
} }
public function getCreatedAt(): ?DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?DateTimeInterface $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setUser($this);
}
return $this;
}
public function removeComment(Comment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getUser() === $this) {
$comment->setUser(null);
}
}
return $this;
}
} }

View File

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\Comment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Comment>
*
* @method Comment|null find($id, $lockMode = null, $lockVersion = null)
* @method Comment|null findOneBy(array $criteria, array $orderBy = null)
* @method Comment[] findAll()
* @method Comment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CommentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Comment::class);
}
// /**
// * @return Comment[] Returns an array of Comment objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Comment
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\DomainPage;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<DomainPage>
*
* @method DomainPage|null find($id, $lockMode = null, $lockVersion = null)
* @method DomainPage|null findOneBy(array $criteria, array $orderBy = null)
* @method DomainPage[] findAll()
* @method DomainPage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class DomainPageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, DomainPage::class);
}
// /**
// * @return DomainPage[] Returns an array of DomainPage objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('d.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?DomainPage
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\PageView;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PageView>
*
* @method PageView|null find($id, $lockMode = null, $lockVersion = null)
* @method PageView|null findOneBy(array $criteria, array $orderBy = null)
* @method PageView[] findAll()
* @method PageView[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PageViewRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PageView::class);
}
// /**
// * @return PageView[] Returns an array of PageView objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?PageView
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Utils;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use function Symfony\Component\String\u;
/**
* This class is used to provide an example of integrating simple classes as
* services into a Symfony application.
* See https://symfony.com/doc/current/service_container.html#creating-configuring-services-in-the-container.
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
final class UserValidator
{
public function validateDisplayName(?string $displayName): string
{
if (empty($displayName)) {
throw new InvalidArgumentException('The display name can not be empty.');
}
if (1 !== preg_match('/^[0-9a-zA-Z _\-]+$/', $displayName)) {
throw new InvalidArgumentException('The display name must contain only latin characters and underscores.');
}
return $displayName;
}
public function validatePassword(?string $plainPassword): string
{
if (empty($plainPassword)) {
throw new InvalidArgumentException('The password can not be empty.');
}
if (u($plainPassword)->trim()->length() < 6) {
throw new InvalidArgumentException('The password must be at least 6 characters long.');
}
return $plainPassword;
}
public function validateEmail(?string $email): string
{
if (empty($email)) {
throw new InvalidArgumentException('The email can not be empty.');
}
if (null === u($email)->indexOf('@')) {
throw new InvalidArgumentException('The email should look like a real email.');
}
return $email;
}
public function validateFullName(?string $fullName): string
{
if (empty($fullName)) {
throw new InvalidArgumentException('The full name can not be empty.');
}
return $fullName;
}
}

View File

@ -213,6 +213,20 @@
"config/packages/security.yaml" "config/packages/security.yaml"
] ]
}, },
"symfony/stimulus-bundle": {
"version": "2.11",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.9",
"ref": "05c45071c7ecacc1e48f94bc43c1f8d4405fb2b2"
},
"files": [
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js"
]
},
"symfony/translation": { "symfony/translation": {
"version": "6.3", "version": "6.3",
"recipe": { "recipe": {
@ -276,6 +290,22 @@
"config/packages/messenger.yaml" "config/packages/messenger.yaml"
] ]
}, },
"symfony/webpack-encore-bundle": {
"version": "2.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"twig/extra-bundle": { "twig/extra-bundle": {
"version": "v3.7.1" "version": "v3.7.1"
} }

View File

@ -1,16 +1,213 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html data-bs-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{% block stylesheets %} {% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %} {% endblock %}
{% block extra_stylesheets %}{% endblock %}
{% block javascripts %} {% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %} {% endblock %}
{% block extra_javascripts %}{% endblock %}
</head> </head>
<body> <body>
{% block body %}{% endblock %} {% if is_granted('ROLE_USER') %}
{# Main Navigation #}
<header>
{# Sidebar #}
<nav id="sidebarMenu" class="collapse d-lg-block sidebar collapse bg-body-tertiary">
<div class="position-sticky">
<div class="list-group list-group-flush mx-3 mt-4">
<a href="#" class="list-group-item list-group-item-action py-2 ripple{{ app.current_route() == 'app_dashboard' ? ' active' : '' }}" aria-current="true">
<i class="fas fa-tachometer-alt fa-fw me-3"></i>
<span>Dashboard</span>
</a>
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
<i class="fas fa-comments fa-fw me-3"></i>
<span>Comments</span>
</a>
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
<i class="fas fa-globe fa-fw me-3"></i>
<span>Domains</span>
</a>
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
<i class="fas fa-users fa-fw me-3"></i>
<span>Users</span>
</a>
<a href="#" class="list-group-item list-group-item-action py-2 ripple">
<i class="fas fa-chart-bar fa-fw me-3"></i>
<span>Stats</span>
</a>
</div>
</div>
</nav>
{# Sidebar #}
{# Navbar #}
<nav id="main-navbar" class="navbar navbar-expand-lg navbar-light bg-body-secondary fixed-top">
{# Container wrapper #}
<div class="container-fluid">
{# Toggle button #}
<button
class="navbar-toggler"
type="button"
data-mdb-toggle="collapse"
data-mdb-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
aria-label="Toggle navigation"
>
<i class="fas fa-bars"></i>
</button>
{# Brand #}
<a class="navbar-brand" href="#">
MyComments
</a>
{# Search form #}
{# <form class="d-none d-md-flex input-group w-auto my-auto">#}
{# <input#}
{# autocomplete="off"#}
{# type="search"#}
{# class="form-control rounded"#}
{# placeholder='Search (ctrl + "/" to focus)'#}
{# style="min-width: 225px"#}
{# />#}
{# <span class="input-group-text border-0"#}
{# ><i class="fas fa-search"></i#}
{# ></span>#}
{# </form>#}
{# Right links #}
<ul class="navbar-nav ms-auto d-flex flex-row">
{# Notification dropdown #}
<li class="nav-item dropdown">
<a
class="nav-link me-3 px-3 me-lg-0 dropdown-toggle hidden-arrow"
href="#"
id="navbarDropdownMenuLink"
role="button"
data-mdb-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-bell"></i>
<span class="badge rounded-pill badge-notification bg-danger"
>1</span
>
</a>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="navbarDropdownMenuLink"
>
<li><a class="dropdown-item" href="#">Some news</a></li>
<li><a class="dropdown-item" href="#">Another news</a></li>
<li>
<a class="dropdown-item" href="#">Something else here</a>
</li>
</ul>
</li>
{# Icon #}
{# <li class="nav-item me-3 me-lg-0">#}
{# <a class="nav-link" href="#">#}
{# <i class="fab fa-github"></i>#}
{# </a>#}
{# </li>#}
{# Icon dropdown #}
{# <li class="nav-item dropdown">#}
{# <a#}
{# class="nav-link me-3 me-lg-0 dropdown-toggle hidden-arrow"#}
{# href="#"#}
{# id="navbarDropdown"#}
{# role="button"#}
{# data-mdb-toggle="dropdown"#}
{# aria-expanded="false"#}
{# >#}
{# <i class="united kingdom flag m-0"></i>#}
{# </a>#}
{# <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">#}
{# <li>#}
{# <a class="dropdown-item" href="#">#}
{# <i class="united kingdom flag"></i>English <i class="fa fa-check text-success ms-2"></i>#}
{# </a>#}
{# </li>#}
{# <li><hr class="dropdown-divider" /></li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="poland flag"></i>Polski</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="china flag"></i>中文</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="japan flag"></i>日本語</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="germany flag"></i>Deutsch</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="france flag"></i>Français</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="spain flag"></i>Español</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="russia flag"></i>Русский</a>#}
{# </li>#}
{# <li>#}
{# <a class="dropdown-item" href="#"><i class="portugal flag"></i>Português</a>#}
{# </li>#}
{# </ul>#}
{# </li>#}
{# Avatar #}
<li class="nav-item dropdown">
<a
class="nav-link dropdown-toggle hidden-arrow d-flex align-items-center"
href="#"
id="navbarDropdownMenuLink"
role="button"
data-mdb-toggle="dropdown"
aria-expanded="false"
>
<img
src="https://mdbootstrap.com/img/Photos/Avatars/img (31).jpg"
class="rounded-circle"
height="24"
alt=""
loading="lazy"
/>
</a>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="navbarDropdownMenuLink"
>
<li><a class="dropdown-item" href="#">My profile</a></li>
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><a class="dropdown-item" href="{{ url('app_logout') }}">Logout</a></li>
</ul>
</li>
</ul>
</div>
{# Container wrapper #}
</nav>
{# Navbar #}
</header>
{# Main Navigation #}
{% endif %}
{% block container %}
<main style="margin-top: 58px">
<div class="container pt-4">
{% block body %}{% endblock %}
</div>
</main>
{% endblock %}
</body> </body>
</html> </html>

View File

@ -3,18 +3,6 @@
{% block title %}Hello DashboardController!{% endblock %} {% block title %}Hello DashboardController!{% endblock %}
{% block body %} {% block body %}
<style> <h4>Dashboard</h4>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } <hr />
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code><a href="{{ '/home/skylar/Projects/MyComments/src/Controller/DashboardController.php'|file_link(0) }}">src/Controller/DashboardController.php</a></code></li>
<li>Your template at <code><a href="{{ '/home/skylar/Projects/MyComments/templates/dashboard/index.html.twig'|file_link(0) }}">templates/dashboard/index.html.twig</a></code></li>
</ul>
</div>
{% endblock %} {% endblock %}

View File

@ -1,20 +1,53 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block body %} {% block container %}
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('app_login') }}" method="post"> <section class="vh-100 gradient-custom">
<label for="username">Email:</label> <div class="container py-5 h-100">
<input type="text" id="username" name="_username" value="{{ last_username }}"> <div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
<div class="card bg-dark text-white" style="border-radius: 1rem;">
<div class="card-body p-5 text-center">
<label for="password">Password:</label> <div class="mb-md-5 mt-md-4 pb-5">
<input type="password" id="password" name="_password"> <form action="{{ path('app_login') }}" method="post">
{# If you want to control the URL the user is redirected to on success <h2 class="fw-bold mb-5">MyComments Login</h2>
<input type="hidden" name="_target_path" value="/account"> #} {% if error %}
<div class="alert alert-danger" role="alert">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
<button type="submit">login</button> <div class="form-outline form-white mb-4">
</form> <input type="email" id="typeEmailX" name="_username" class="form-control form-control-lg" value="{{ last_username }}" />
{% endblock %} <label class="form-label" for="typeEmailX">Email</label>
</div>
<div class="form-outline form-white mb-4">
<input type="password" id="typePasswordX" name="_password" class="form-control form-control-lg" />
<label class="form-label" for="typePasswordX">Password</label>
</div>
<button class="btn btn-outline-light btn-lg px-5" type="submit">Login</button>
<div class="d-flex justify-content-center text-center mt-4 pt-1">
<a href="#" class="text-white"><i class="fab fa-google fa-lg mx-1"></i></a>
<a href="#" class="text-white"><i class="fab fa-github fa-lg mx-1"></i></a>
</div>
</form>
</div>
<div>
<p class="mb-0"><a href="#!" class="text-white-50 fw-bold">Forgot your password?</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

79
webpack.config.js Normal file
View File

@ -0,0 +1,79 @@
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/app.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json')
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// configure Babel
// .configureBabel((config) => {
// config.plugins.push('@babel/a-babel-plugin');
// })
// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.23';
})
// enables Sass/SCSS support
//.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use React
//.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
.autoProvidejQuery()
.enableSassLoader()
;
module.exports = Encore.getWebpackConfig();

7037
yarn.lock Normal file

File diff suppressed because it is too large Load Diff