Lorsque vous créez une image Docker, vous souhaitez peut-être la rendre facilement accessible à vous et au monde entier. La plupart d'entre nous ont été habitués à GitHub pour partager du code et, aujourd'hui, à DockerHub pour rendre nos images Docker accessibles au public.
Auparavant, nous pouvions connecter nos comptes GitHub et DockerHub et demander à DockerHub d'écouter les modifications apportées à notre code pour reconstruire et publier les nouvelles images Docker. Malheureusement, ils ont changé leur modèle. Il faut un abonnement premium pour faire cela.
Nous pouvons toujours reconstruire et pousser toutes les modifications de notre système local vers DockerHub, mais cela est fastidieux et rend un peu plus compliqué de garder un œil sur l'historique de notre image. Heureusement, nous pouvons utiliser les actions GitHub pour effectuer ces tâches à notre place lorsque nous poussons les changements vers le dépôt.
Commençons par mettre en place un dépôt Git et le connecter à notre compte GitHub pour télécharger toutes les données. Tout d'abord, dans le terminal de l'ordinateur, nous devons créer un répertoire et l'initialiser en tant que dépôt Git.
mkdir calculate-pi-image
cd calculate-pi-image
git init
Ensuite, nous devons nous connecter au compte GitHub et démarrer un nouveau dépôt, nous pouvons l'appeler comme nous voulons, disons calculate-pi.
Dans le terminal, nous pouvons maintenant connecter notre répertoire local à notre dépôt GitHub distant.
git remote add origin git@github.com:DennisdeBest/calculate-pi.git
git branch -M main
Ajoutons un Dockerfile
basique et poussons-le vers le dépôt distant.
echo "FROM alpine:3.15" > Dockerfile
git add . && git commit -m "Init"
git push --set-upstream origin main
Ce fichier est maintenant disponible sur le dépôt public GitHub, mais ce n'est pas encore une image Docker construite et pas du tout disponible sur DockerHub pour le moment.
Nous devons maintenant configurer un compte DockerHub. Nous devons définir, et garder à l'esprit, le DockerID.
Ce sera la base du nom qui sera nécessaire pour obtenir nos images.
Nous pouvons maintenant construire nos images Docker localement et les pousser vers DockerHub. Construisons donc notre image de base.
L'image doit avoir un nom avec [dockerId]/[image-name] :[tag]
, Dans mon cas, ce sera debst/calculate-pi:latest
.
docker build -f Dockerfile . -t debst/calculate-pi:latest
Nous pouvons maintenant pousser cette image pour la rendre disponible sur notre compte DockerHub. Tout d'abord, nous devons nous assurer que nous sommes connectés. Il nous sera demandé notre ID Docker et notre mot de passe.
docker login
Nous pouvons ainsi pousser notre image.
docker push debst/calculate-pi:latest
Cela fonctionne maintenant et l'image est disponible. Cependant, il s'agit de quelques actions que nous devrons effectuer à chaque mise à jour. Faisons en sorte que GitHub le fasse pour nous chaque fois que nous envoyons de nouvelles mises à jour au dépôt.
Comme je l'ai mentionné au début de cet article, il était autrefois facile de dire à DockerHub de garder un œil sur tout changement dans le dépôt public GitHub et de reconstruire...
]]>PHP 8.1 a ajouté Enums, ce qui nous permet de définir un nombre défini de valeurs possibles pour un tableau. C'est un moyen très intéressant de s'assurer que seules les valeurs que nous voulons peuvent être à une valeur spécifique.
J'ai donc commencé à jouer avec eux il y a quelque temps et j'ai rencontré quelques problèmes avec Doctrine et le Symfony FormType.
Plongeons-y !
J'ai écrit un petit programme pour générer des captures d'écran du navigateur lorsqu'on lui donne une certaine configuration. Il utilise browsershot. Je voulais une page web avec un formulaire pour toutes les différentes options de configuration, l'une d'entre elles étant le type de sortie.
J'ai donc créé un Enum FileType
:
<?php
namespace App\Enum;
enum FileType: string
{
case PNG = 'image/png';
case JPEG = 'image/jpeg';
case PDF = 'application/pdf';
}
Il y a 3 cas qui ont une valeur, c'est ce qu'on appelle une Enum. Vous pouvez accéder à ces valeurs sur une Enum en appelant simplement la propriété value.
$fileType = FileType::PNG;
$mimeType = $fileType->value; // 'image/png'
Avec cela, je voulais ajouter quelques fonctions simples pour vérifier quelle sera l'extension du fichier lorsqu'un certain type est utilisé et pour vérifier si la valeur sélectionnée est une image.
enum FileType: string
{
...
public static function isImage(?self $value): bool
{
return match ($value) {
self::PDF, null => false,
self::PNG, self::JPEG => true,
};
}
public static function getExtension(?self $value): ?string
{
return match ($value) {
self::PDF => 'pdf',
self::PNG => 'png',
self::JPEG => 'jpeg',
};
}
}
Je peux maintenant les utiliser facilement dans mon contrôleur Symfony pour définir le nom de fichier lors du téléchargement du fichier.
#[Route('/shot/{shot}/file', name: 'shot_file')]
public function getShotFile(Shot $shot, Request $request): StreamedResponse
{
...
$fileType = $configuration->getFileType();
$mimeType = $fileType->value;
$extension = FileType::getExtension($fileType);
$filename = 'browserShot-' . (new \DateTime())->getTimestamp() . '.' . $extension;
...
}
Et dans mes modèles Twig.
{% if shotConfiguration.isImage %}
<img src="{{ shot.base64 }}">
{% else %}
<iframe src="{{ shot.base64 }}"></iframe>
{% endif %}
Il est agréable de pouvoir accéder facilement à ces fonctions sur tout ce qui utilise cette Enum comme propriété. La prochaine chose dont j'avais besoin était un moyen facile d'obtenir toutes les valeurs et les cas d'une Enum pour les messages d'erreur, j'ai donc ajouté quelques fonctions supplémentaires.
public static function getCases(): array
{
$cases = self::cases();
return array_map(static fn(UnitEnum $case) => $case->name, $cases);
}
public static function getValues(): array
{
$cases = self::cases();
return array_map(static fn(UnitEnum $case) => $case->value, $cases);
}
Maintenant, dans ma commande Symfony, je peux ajouter une erreur si le type de fichier qui est défini ne fait pas partie des types disponibles.
$io->error(sprintf('The file type you provided, %s, is not part of the available file types : [%s]', $fileTypeInput, implode(', ',FileType::getValues())));
Ou les cas si c'est ce que la commande attend.
->addOption('format', null, InputOption::VALUE_OPTIONAL, 'Set the window to a format, allowed formats are ' . implode(', ',PaperFormat::getCases()))
Avec cela, j'ai maintenant une belle commande pour générer les captures d'écran, qui n'accepte...
]]>Dans l'article précédent, j'ai ajouté quelques animations CSS simples pour montrer comment fonctionne la séparation de mes assets. Maintenant que tout cela est en place, plongeons un peu plus dans le JavaScript et créons une animation de machine à écrire encore meilleure en fonction de vos données !
Pour que nos CSS et JavaScript interagissent avec cette page de blog, nous devons mettre en place certains éléments HTML, comme j'utilise l'éditeur de contenu Grav, je dois saisir certains éléments en particulier.
<label for="typewriter-input-data">Input</label>
<textarea id='typewriter-input-data'>Default input data</textarea>
<label for="delay-input">Delay <small>(ms)</small></label>
<input type="number" id="delay-input" name="speed" value="50"/>
<br>
<button class="type-btn" id="type-btn">Type !</button>
<button class="type-btn" id="cancel-btn" disabled>Cancel</button>
<ul id="typewriter-errors" class="errors"></ul>
<div id="typewriter-errors" class="banner"></div>
J'ai maintenant :
D'abord, je vais chercher tous les éléments dont j'ai besoin :
export default class Typewriter {
private typeWriterInput: HTMLTextAreaElement
private typeWriterOutput: HTMLElement
private typeButton: HTMLButtonElement;
private delayInput: HTMLInputElement;
private errorElement: HTMLUListElement;
private cancelButton: HTMLButtonElement;
constructor() {
this.typeWriterInput = document.getElementById('typewriter-input-data') as HTMLTextAreaElement
this.typeWriterOutput = document.getElementById('typewriter-output') as HTMLElement
this.typeButton = document.getElementById('type-btn') as HTMLButtonElement
this.cancelButton = document.getElementById('cancel-btn') as HTMLButtonElement
this.delayInput = document.getElementById('delay-input') as HTMLInputElement
this.errorElement = document.getElementById('typewriter-errors') as HTMLUListElement
}
}
Ensuite, nous devons faire en sorte que quelque chose se passe dès que le bouton Type !
est cliqué, dans le constructeur j'ai ajouté l'écouteur d'événement qui appellera une autre fonction de ma classe, typeEvent
:
export default class Typewriter {
constructor() {
//...
this.typeButton.addEventListener('click', () => {
this.typeEvent()
})
//...
}
typeEvent() {
this.resetTypewriter()
let delay = <any>this.delayInput.value as number;
let errors: string[] = [];
if (delay < 10) {
errors.push("Delay has to be...
]]>
“Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.” Linus Torvalds
]]>Comme j'ai récemment ajouté [TypeScript et WebPack encore à Grav] (/blog/adding-webpack-encore-and-typescript-to-grav), j'ai voulu trouver un moyen d'ajouter CSS et JavaScript uniquement à certaines pages.
Jusqu'à présent, j'ai généré une page assets.html
qui contenait les liens vers les fichiers JavaScript et CSS générés par
WebPack :
<script defer src="/user/themes/lingonberry-custom/build/app/app.js"></script>
<link href="/user/themes/lingonberry-custom/build/app/app.css" rel="stylesheet">
Pour générer différents fichiers, j'ai modifié le fichier webpack.config.js
. J'ai trouvé la plupart des informations dans
la [documentation Symfony] (https://symfony.com/doc/current/frontend/encore/advanced-config.html).
Cependant, comme je n'aime pas trop me répéter, j'ai créé une fonction :
function generateBaseConfig(name, buildPath, inputFile, outputFile, cleanOutput = true) {
let itemBuildPath = path.join(BUILD_PATH_BASE, name);
if (buildPath) {
itemBuildPath = path.join(BUILD_PATH_BASE, buildPath);
}
if (!inputFile) {
inputFile = `${name}.ts`
}
if (!outputFile) {
outputFile = 'assets.html'
}
let config = Encore
// directory where compiled assets will be stored
.setOutputPath(itemBuildPath)
// public path used by the web server to access the output path
.setPublicPath(path.join('/', itemBuildPath))
.addEntry(name, `./${path.join(PATH, 'js', inputFile)}`)
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
//.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
//.enableSingleRuntimeChunk()
.disableSingleRuntimeChunk()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
.addPlugin(new CompressionPlugin())
// .addPlugin(new MiniCssExtractPlugin())
.addPlugin(new HtmlWebpackPlugin({
inject: false,
filename: outputFile,
publicPath: path.join('/', itemBuildPath),
scriptLoading: 'defer',
templateContent: ({htmlWebpackPlugin}) => `
${htmlWebpackPlugin.tags.headTags}
`
}))
// enables @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = 3;
})
.enableSassLoader()
.enableTypeScriptLoader();
if (cleanOutput) {
config.cleanupOutputBeforeBuild()
}
return config.getWebpackConfig()
}
Donc maintenant pour ajouter plusieurs entrées/sorties je n'ai plus qu'à ajouter quelques lignes :
const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH_BASE = path.join(PATH, 'build')
const baseEncoreConfiguration = generateBaseConfig('app');
baseEncoreConfiguration.name = 'baseEncoreConfiguration'
Encore.reset();
const animationsConfiguration = generateBaseConfig('animations');
animationsConfiguration.name = 'animationsConfiguration'
module.exports = [baseEncoreConfiguration, animationsConfiguration];
Si vous ajoutez seulement un nom lors de l'appel de la fonction generateBaseConfig
, l'entrée sera
de {{themeName}}/js/{{name}}.ts
et la sortie sera {{themeName}}/build/{{name}}/assets.html
. donc dans l'exemple précédent j'avais les fichiers suivants :
user/themes/lingonberry-custom
└── js
├── animations.ts
└── app.ts
Ce qui a généré la sortie suivante :
user/themes/lingonberry-custom
├── build
├── animations
│ ├── animations.css
│ ├── animations.js
│ ├── animations.js.gz
│ ├── assets.html
│ ├── assets.html.gz
│ ├── entrypoints.json
│ ├── manifest.json
│ └── manifest.json.gz
├── app
│ ├── app.css
│ ├── app.css.gz
│ ├── app.js
│ ├── app.js.gz
│ ├── assets.html
│ ├── assets.html.gz
│ ├── entrypoints.json
│ ├── fonts
│ ├── images
│ ├── manifest.json
│ └── manifest.json.gz
├── entrypoints.json
├── manifest.json
└── manifest.json.gz
Chaque entrée de fichier a un répertoire de sortie séparé. Sans cela la fonction cleanupOutputBeforeBuild
d'Encore viderait toujours le répertoire principal de construction. Comme les constructions se déroulent en parallèle, il n'en resterait toujours qu'un à la fin,
les autres étant supprimés.
Maintenant qu'ils sont générés, les fichiers de sortie doivent être inclus...
]]>Nous avons tous l'habitude de voir une barre de défilement sur toutes les pages que nous visitons quotidiennement sur Internet.
Ils sont utiles et explicites. C'est très pratique mais pas agréable.
Depuis un certain temps maintenant, tous les principaux navigateurs acceptent certains pseudo éléments
CSS à personnaliser en fonction de l'apparence du défilement du navigateur.
Comme j'ai quelques morceaux de code qui défileront sur l'axe X et la page d'accueil qui défile sur l'axe Y, je voulais que cela soit un peu plus, approprié, avec le reste du site.
Pour ce faire, j'ai ajouté du CSS
à mon thème.
Si vous avez vu mon article précédent sur l'ajout de TypeScript et SCSS à Grav, vous n'aurez qu'à importer un autre script dans votre SCSS principal. Partout ailleurs, vous devrez ajouter le CSS suivant :
/* components/sidebar.scss */
@use '../variables' as *;
/* width */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* Track */
::-webkit-scrollbar-track {
background: $bg-color;
outline: $primary-color-dark solid 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: $primary-color;
outline: $primary-color-dark;
border-radius: 50vh;
transition: 1s ease-in-out;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: $primary-color-dark;
border: 1px solid $primary-color;
}
Lors de la rédaction de cet article, Firefox ne prend pas en charge ces pseudos éléments CSS. Il prend cependant en charge une propriété depuis la version 64 qui nous rapprochera de ce que nous avons sur les autres navigateurs. J'ai ajouté ceci à la fin de mon fichier précédent :
/* Firefox properties */
* {
scrollbar-color: $primary-color $bg-color;
}
Les couleurs sont maintenant correctes mais malheureusement on ne peut pas changer le rayon de la bordure pour l'instant.
Dans mon cas, ce fichier se trouve dans mon répertoire de composants appelé sidebar.scss
, il importe un fichier SCSS contenant des variables de son répertoire parent :
$bg-color: #f1f1f1;
$bg-color-dark: darken($bg-color, 15%);
$primary-color: #ff6a1e;
$primary-color-dark: darken($primary-color, 15%);
Cela sera ensuite inclus dans mon fichier SCSS principal pour m'assurer qu'il sera ajouté à mon modèle HTML principal grâce à WebPack Encore
@use 'components/scrollbar';
Grâce à cela, mes barres de défilement seront désormais beaucoup plus proches du style de mon site :
Parfois, pendant que j'écris du code, je perds la notion du temps et je n'ai pas vraiment d'idée du temps que j'ai passé.
Pour avoir plus d'informations sur cela je peux m'acheter un chronomètre ou noter l'heure du départ dans un carnet. C'est un peu trop manuel et il serait difficile de garder un journal.
Donc pour être proche de mon ordinateur j'ai écrit un script Bash qui va garder un œil sur le temps passé. Dès que je lui dis que je démarre quelque chose il va m'afficher le temps passé dessus et à la fin il va écrire àa dans un fichier journal.
Pour créer ce nouveau script j'utilise mon générateur de scripts bash localScript.
localScript timer
#!/usr/bin/env bash
if [[ -z "$1" ]]; then
echo "You need to set a project name as the first variable"
exit 0
fi
PROJECT=$1
FILENAME='time.log'
START_TIME=$SECONDS
Je passe le nom du fichier journal dans lequel je veux écrire et je m'assure que le nom du projet est bien passé quand le commande est lancé.
Le script doit continuer à tourner jusqu'a ce que je l'arrête, et à ce moment la, écrire dans le journal. Pour ce faire j'ai créé une boucle while true
:
while true
do
DURATION=$(( SECONDS - $START_TIME ))
echo "$DURATION"
sleep 10
done
Ensuis j'ai ajouté une commande trap pour intercepter l'arrêt du script et écrire dans le journal :
function writeLog()
{
echo "$PROJECT date: $(date +'%d-%m-%Y') / user: $USER / time: $DURATION" >> $FILENAME
}
trap writeLog EXIT
En lançant la commande timer "test project"
on voit ce qui suit dans le terminal :
timer "test project"
test project time 0
test project time 10
test project time 20
Dès qu'on arrête le script avec un CTRL+C
une ligne sera rajouté dans le journal time.log :
test project date: 18-03-2021 / user: dennis / time: 12421
Cela affiche la date, le nom de l'utilisateur et le temps passé sur le projet.
C'est un bon début mais afin de le rendre un peu plus lisible j'ai passé le temps en secondes en string contenant les heures, minutes et secondes avec une fonction :
function secondsToTimeString()
{
echo "$(($1/3600))h:$(($1%3600/60))m:$(($1%60))s"
}
Je ne veux pas non plus spammer mon terminal sur pleins de lignes donc j'ai ajouté l'option -ne
à mon echo
:
echo -ne "$PROJECT time $(secondsToTimeString $DURATION) \r"
Maintenant ça marche bien et nous pouvons aussi attacher cette commande à d'autres pour garder un œil sur le temps passé. Voici le script en entier :
#!/usr/bin/env bash
if [[ -z "$1" ]]; then
echo "You need to set a project name as the first variable"
exit 0
fi
PROJECT=$1
FILENAME='time.log'
START_TIME=$SECONDS
function secondsToTimeString()
{
echo "$(($1/3600))h:$(($1%3600/60))m:$(($1%60))s"
}
function writeLog()
{
echo "$PROJECT date: $(date +'%d-%m-%Y') / user: $USER / time: $(secondsToTimeString $DURATION) [$DURATION]" >> $FILENAME
}
trap writeLog EXIT
while true
do
DURATION=$(( SECONDS - $START_TIME ))
echo -ne "$PROJECT time $(secondsToTimeString $DURATION) \r"
sleep 10
done
Si vous lancez le script vous verrez :...
]]>J'ai commencé ce blog, Grav, en 2016. C'était une façon pratique, sympathique et facile de mettre les choses en place.
J'ai pris le theme LingonBerry et seulement changé quelques lignes du code CSS et quelques couleurs. Cela à bien fonctionné pendant des années.
Récemment j'ai voulu revenir sur mon blog et ajouter du contenu, j'ai regardé le code qui était en place.
Le theme n'utilise que du CSS
à plat ainsi que du JS
pour son style et son interaction avec les utilisateurs.
C'est une base sympa mais, depuis les années, je me suis habitué à TypeScript et SCSS. Cela rend le développement plus rapide et sécurisé mais le navigateur ne comprend pas ce code en dur, il faut donc trouver un moyen de lui traduire tout ça.
Je me suis habitué à WebPack Encore avec mes projets Symfony et il avait tendance à s'occuper de tout. Pour voir comment implémenter cela dans un projet Grav, j'ai lu la Documentation et installé webpack encore avec yarn :
yarn add @symfony/webpack-encore --dev
D'habitude quand je l'installe dans un projet symfony avec Flex il génère les fichiers de configuration de base. Ici ce n'est pas le cas donc j'ai créé mon propre webpack.config.js
à la racine de mon projet :
const Encore = require('@symfony/webpack-encore');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 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');
}
const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH = path.join(PATH, 'build')
Encore
// directory where compiled assets will be stored
.setOutputPath(BUILD_PATH)
// public path used by the web server to access the output path
.setPublicPath(path.join('/', BUILD_PATH))
.addEntry('app', path.join('./', PATH, 'js', 'app.ts'))
.disableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
.addPlugin(new CompressionPlugin())
// .addPlugin(new MiniCssExtractPlugin())
.addPlugin(new HtmlWebpackPlugin({
inject: false,
filename: 'assets.html',
publicPath: path.join('/', BUILD_PATH),
scriptLoading: 'defer',
templateContent: ({htmlWebpackPlugin}) => `
${htmlWebpackPlugin.tags.headTags}
`
}))
// enables @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = 3;
})
.enableSassLoader()
.enableTypeScriptLoader()
module.exports = Encore.getWebpackConfig();
J'ai rajouté quelques plugins pour la compilation du SASS et du TS et quelque uns d'autres que nous verrons plus tard. Ensuite j'ai rajouté des variables :
const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH = path.join(PATH, 'build')
Le theme de mon site se trouve dans le répertoire user/themes/lingonberry-custom
, J'ai donc voulu mettre tout ça dans une variable pour pouvoir changer de thème un jour sans avoir à tout réécrire dans mon fichier de configuration WebPack.
Il faut dire à WebPack quels fichiers regarder et où les mettre une fois traduits. Avec Grav le 'outputPath' et 'publicPath' sont pratiquement au même endroit car ils partagent leur racine de répertoire (tous les fichiers servis par le serveur front sont dans le même répertoire que tous les fichiers php qui génèrent le back).
.setOutputPath(BUILD_PATH)
.setPublicPath(path.join('/', BUILD_PATH))
.addEntry('app', path.join('./', PATH, 'js', 'app.ts'))
J'ai rajouté un fichier app.ts...
Un des sites sur lesquels j'ai un formulaire de contact Symfony commence à recevoir pas mal de messages de Spam.
Pour éviter cela je peux mettre en place un reCaptcha mais je préfère essayer de régler les problèmes de mon côté avant d'importer encore plus d'éléments.
Mon formulaire de départ était plus tôt simple :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class,
[
'required' => false,
'label' => 'form.contact.name',
'translation_domain' => 'Default'
]
)
->add('email', EmailType::class,
[
'required' => false,
'label' => 'form.contact.email',
'translation_domain' => 'Default',
]
)
->add('url', UrlType::class,
[
'required' => false,
'label' => 'form.contact.url',
'translation_domain' => 'Default'
]
)
->add('message', TextType::class,
[
'required' => false,
'label' => 'form.contact.message',
'translation_domain' => 'Default'
]
);
}
Basé sur mon entité Message
:
/**
* @ORM\Entity(repositoryClass="App\Repository\MessageRepository")
*/
class Message
{
use TimestampableEntity;
private $id;
private $name;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank
* @Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* mode="strict"
* )
*/
private $email;
private $url;
private $message;
// ...
}
Le formulaire est envoyé par du AJAX et traité dans un Controlleur :
/**
* @Route("message", name="send_message", options={"expose"=true}, methods={"POST"})
* @param Request $request
* @param TranslatorInterface $translator
* @return JsonResponse
*/
public function sendMessage(Request $request, TranslatorInterface $translator): JsonResponse
{
$messageForm = $this->createForm(MessageType::class, null);
$messageForm->handleRequest($request);
if ($messageForm->isSubmitted() && $messageForm->isValid()) {
/** @var Message $message */
$message = $messageForm->getData();
$this->manager->persist($message);
$this->manager->flush();
return new JsonResponse($translator->trans('form.contact.sent', [], 'Default'));
} else {
$errors = $this->getErrorsFromForm($messageForm);
return new JsonResponse($errors, Response::HTTP_BAD_REQUEST);
}
}
Maintenant rajoutons des choses pour perturber les robots !
J'imagine que les robots reconnaissent les inputs qui ont email
en nom et adorent les remplir.
Je vais donc garder mon champ email et rajouter un champ qui à un nom aléatoire qui détiendra le vrai email de l'envoyeur s’il a bien rempli le formulaire.
Le nouveau champ de mon entité Message
a les assertions d'email, et le champ email ne les a plus :
class Message
{
public const EMAIL_HIDDEN_INPUT = 'fhjgiz46';
//...
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @Assert\NotBlank
* @Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* mode="strict"
* )
*/
private $fhjgiz46;
//...
}
Ce nouveau champ n'est pas mappé et ne sera donc pas présent en base de données. J'ai rajouté ce champ dans mon FormType
:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//...
->add('email', EmailType::class, ['required' => false, 'attr' => ['class' => 'email']])
->add(Message::EMAIL_HIDDEN_INPUT, EmailType::class,
[
'required' => false,
'label' => 'form.contact.email',
'translation_domain' => 'Default',
]
)
//...
}
Ce champ aura une classe css .email
sur lequel je rajoute quelques règles afin de le rendre invisible pour l'homme :
input.email {
height: 0;
margin: 0;
text-decoration: none;
border: none;
}
Dans le controlleur je veux vérifier si l'input email
est rempli. Si c'est le cas ça veux dire que c'est un robot qui essaie de remplir le formulaire et nous ne voulons pas envoyer d'email.
S’il n'est pas rempli mais le nouveau...
]]>Beaucoup de gens sont encore habitués à ce qu'un site web commence par www.
mais ce n'est plus le cas. Cela a créé quelques problèmes dans le passé où j'ai déployé un site à https://my-site.org
mais ensuite quand les gens essayaient de partager le site, ils le mentionnaient comme https://www.my-site.org
.
J'ai donc commencé à chercher comment rendre le site disponible sur ces deux URLs. La première chose qui m'est venue à l'esprit était de modifier les étiquettes Traefik
attachées au conteneur Docker qui sert le site.
# docker-compose.yaml
version : '3.7'
services :
nginx :
...
labels :
- "traefik.enable=true"
- "traefik.http.routers.my-site.rule=Host(`my-site.org`) || Host(`www.my-site.org`)"
Cela fonctionne et le site devient disponible sur les deux URLs, super tout est fait, il est temps d'aller au lit ...
Le problème est que les robots d'exploration des sites considèrent que les données du site sont dupliquées et ils n'aiment vraiment pas ça. Pour éviter que cela ne se produise, nous devons nous assurer que toute personne ou tout objet essayant d'aller sur une page commençant par www.
soit automatiquement et définitivement redirigé vers la page sans ce sous-domaine.
J'ai commencé par faire tourner une petite instance de nGinx dans un autre conteneur étiqueté avec le préfixe www.
qui renverrait un 301 vers le site actuel. C'était un peu compliqué et cela commençait à devenir ennuyeux à faire pour chaque site.
En fonction de l'hôte auquel le site est enregistré, je pouvais parfois ajouter une redirection aux paramètres DNS, mais pas toujours, et cela demandait toujours beaucoup de clics.
Heureusement, en lisant les mises à jour de Traefik, j'ai trouvé quelque chose.
Je suis tombé sur le RedirectRegex middleware section of the documentation. Il indique que si la requête entrante correspond à une regex, nous remplaçons l'url de la requête. Je me suis donc dit que j'allais tenter le coup pour me débarrasser du sous-domaine www.
Il existe de nombreuses façons d'ajouter des middlewares à votre configuration Traefik, mais comme celui-ci sera de plus en plus souvent requis, je ne voulais pas ajouter beaucoup d'étiquettes supplémentaires à chaque conteneur Docker. J'ai fini par l'ajouter à mon fichier dynamic_conf.yaml
.
http :
middlewares :
redirect-www :
redirectRegex :
regex : "^https?://www\\.(.+)"
replacement : "https://${1}"
permanent : true
Et voilà ! Tout ce qui commence par http://www. ou https://www. sera redirigé de façon permanente vers https://.
http://www.my-site.org => https://my-site.org
Donc maintenant, si je veux qu'un de mes conteneurs utilise ce middleware, je dois ajouter une autre étiquette :
labels :
- "traefik.enable=true"
- "traefik.http.routers.my-site.rule=Host(`my-site.org`) || Host(`www.my-site.org`)"
- "traefik.http.routeurs.mon-site.middlewares=redirect-www@file"
Updates
^https?://www.(.*)
à ^https?://www\\.(.+)
grâce aux commentaires de Navossoc