feat: upgrade to symfony 7.4

This commit is contained in:
2026-09-01 11:24:44 +02:00
parent 8ed20db90c
commit 2459af122f
41 changed files with 1527 additions and 1219 deletions
+2 -1
View File
@@ -26,7 +26,8 @@ APP_SECRET=b3cb0285aea14bbffb57df0cc3aa84f4
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" # DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=15&charset=utf8" # DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=15&charset=utf8"
DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.4.5&charset=utf8mb4"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/messenger ### ###> symfony/messenger ###
+1
View File
@@ -4,6 +4,7 @@
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php
/config/reference.php
/public/bundles/ /public/bundles/
/var/ /var/
/vendor/ /vendor/
+37 -36
View File
@@ -10,7 +10,7 @@
} }
], ],
"require": { "require": {
"php": ">=8.1", "php": ">=8.2",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
@@ -29,7 +29,7 @@
"liip/imagine-bundle": "^2.11", "liip/imagine-bundle": "^2.11",
"maennchen/zipstream-php": "^3.1", "maennchen/zipstream-php": "^3.1",
"nelmio/security-bundle": "^3.0", "nelmio/security-bundle": "^3.0",
"nesbot/carbon": "^2.70", "nesbot/carbon": "^3.8",
"oneup/uploader-bundle": "^5.1", "oneup/uploader-bundle": "^5.1",
"phpdocumentor/reflection-docblock": "^5.3", "phpdocumentor/reflection-docblock": "^5.3",
"phpoffice/phpspreadsheet": "^5.9", "phpoffice/phpspreadsheet": "^5.9",
@@ -41,38 +41,38 @@
"spatie/blink": "^1.4", "spatie/blink": "^1.4",
"spatie/icalendar-generator": "^2.5", "spatie/icalendar-generator": "^2.5",
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*", "symfony/asset": "7.4.*",
"symfony/console": "6.4.*", "symfony/console": "7.4.*",
"symfony/doctrine-messenger": "6.4.*", "symfony/doctrine-messenger": "7.4.*",
"symfony/dotenv": "6.4.*", "symfony/dotenv": "7.4.*",
"symfony/expression-language": "6.4.*", "symfony/expression-language": "7.4.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "6.4.*", "symfony/form": "7.4.*",
"symfony/framework-bundle": "6.4.*", "symfony/framework-bundle": "7.4.*",
"symfony/http-client": "6.4.*", "symfony/http-client": "7.4.*",
"symfony/intl": "6.4.*", "symfony/intl": "7.4.*",
"symfony/mailer": "6.4.*", "symfony/mailer": "7.4.*",
"symfony/mailjet-mailer": "6.4.*", "symfony/mailjet-mailer": "7.4.*",
"symfony/mime": "6.4.*", "symfony/mime": "7.4.*",
"symfony/monolog-bundle": "^3.0", "symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.4.*", "symfony/notifier": "7.4.*",
"symfony/process": "6.4.*", "symfony/process": "7.4.*",
"symfony/property-access": "6.4.*", "symfony/property-access": "7.4.*",
"symfony/property-info": "6.4.*", "symfony/property-info": "7.4.*",
"symfony/runtime": "6.4.*", "symfony/runtime": "7.4.*",
"symfony/security-bundle": "6.4.*", "symfony/security-bundle": "7.4.*",
"symfony/serializer": "6.4.*", "symfony/serializer": "7.4.*",
"symfony/stimulus-bundle": "^2.11", "symfony/stimulus-bundle": "^2.11",
"symfony/string": "6.4.*", "symfony/string": "7.4.*",
"symfony/translation": "6.4.*", "symfony/translation": "7.4.*",
"symfony/twig-bundle": "6.4.*", "symfony/twig-bundle": "7.4.*",
"symfony/uid": "6.4.*", "symfony/uid": "7.4.*",
"symfony/ux-twig-component": "^2.16", "symfony/ux-twig-component": "^2.16",
"symfony/validator": "6.4.*", "symfony/validator": "7.4.*",
"symfony/web-link": "6.4.*", "symfony/web-link": "7.4.*",
"symfony/webpack-encore-bundle": "^2.0", "symfony/webpack-encore-bundle": "^2.0",
"symfony/workflow": "6.4.*", "symfony/workflow": "7.4.*",
"symfony/yaml": "6.4.*", "symfony/yaml": "7.4.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/html-extra": "^3.7", "twig/html-extra": "^3.7",
"twig/intl-extra": "^3.7", "twig/intl-extra": "^3.7",
@@ -126,7 +126,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": true, "allow-contrib": true,
"require": "6.4.*" "require": "7.4.*"
} }
}, },
"require-dev": { "require-dev": {
@@ -135,13 +135,14 @@
"friendsofphp/php-cs-fixer": "^3.84", "friendsofphp/php-cs-fixer": "^3.84",
"marcocesarato/php-conventional-changelog": "^1.17", "marcocesarato/php-conventional-changelog": "^1.17",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.4.*", "rector/rector": "^2.6",
"symfony/css-selector": "6.4.*", "symfony/browser-kit": "7.4.*",
"symfony/debug-bundle": "6.4.*", "symfony/css-selector": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/maker-bundle": "^1.0", "symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^6.3", "symfony/phpunit-bridge": "^7.4",
"symfony/stopwatch": "6.4.*", "symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "6.4.*" "symfony/web-profiler-bundle": "7.4.*"
}, },
"version": "0.2.5" "version": "0.2.5"
} }
Generated
+1347 -1029
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -8,6 +8,13 @@ doctrine:
#server_version: '15' #server_version: '15'
orm: orm:
auto_generate_proxy_classes: true auto_generate_proxy_classes: true
# Keep implicit controller-argument entity resolution on (many actions type-hint
# an entity against a matching {id}/{uuid} route param without #[MapEntity]).
# Setting this explicitly silences the "default will flip to false" deprecation;
# the separate "automapping is deprecated" notice stays until those actions are
# migrated to #[MapEntity], which is its own task.
controller_resolver:
auto_mapping: true
enable_lazy_ghost_objects: true enable_lazy_ghost_objects: true
report_fields_where_declared: true report_fields_where_declared: true
validate_xml_mapping: true validate_xml_mapping: true
+5
View File
@@ -0,0 +1,5 @@
framework:
property_info:
# Keep the current (7.x) default; enabling the constructor extractor is a
# behaviour change to evaluate on its own, not part of the 6.4 -> 7.4 bump.
with_constructor_extractor: false
+1 -1
View File
@@ -1,4 +1,4 @@
when@dev: when@dev:
_errors: _errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml' resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error prefix: /_error
+2 -2
View File
@@ -1,8 +1,8 @@
when@dev: when@dev:
web_profiler_wdt: web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt prefix: /_wdt
web_profiler_profiler: web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler prefix: /_profiler
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml')
// Upgrade rules bound to the Symfony / Twig / Doctrine versions currently
// installed (resolved from composer.lock). Bump the constraints and run
// `composer update` before executing Rector so it targets 7.x.
->withComposerBased(twig: true, doctrine: true, symfony: true)
->withSets([
SymfonySetList::CONFIGS,
])
->withSkip([
// Generated migrations are frozen once shipped.
__DIR__ . '/migrations',
__DIR__ . '/src/**/DoctrineMigrations',
])
->withParallel();
+1 -1
View File
@@ -45,7 +45,7 @@ class DownloadController extends AbstractController
} }
} }
$inline = (bool) $request->get('inline'); $inline = $request->query->getBoolean('inline');
$disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT; $disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT;
// mark file downloaded if applicable // mark file downloaded if applicable
@@ -27,13 +27,8 @@ class PasswordResetController extends AbstractController
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'label' => 'E-Mail', 'label' => 'E-Mail',
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank(message: 'Bitte angeben'),
'message' => 'Bitte angeben', new Email(mode: 'strict', message: 'Ungültige E-Mail-Adresse'),
]),
new Email([
'mode' => 'strict',
'message' => 'Ungültige E-Mail-Adresse',
]),
], ],
]) ])
->getForm() ->getForm()
+1 -1
View File
@@ -19,7 +19,7 @@ class DeleteController extends AbstractController
#[IsGranted('ROLE_USER')] #[IsGranted('ROLE_USER')]
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
$uuid = $request->get('uuid'); $uuid = $request->query->get('uuid');
$this->uploadHandler->removeUploadFromSession($uuid); $this->uploadHandler->removeUploadFromSession($uuid);
+1 -1
View File
@@ -24,7 +24,7 @@ class Contact
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Bitte gib die E-Mail-Adresse an')] #[Assert\NotBlank(message: 'Bitte gib die E-Mail-Adresse an')]
#[Assert\Email(message: 'Bitte gib eine gültige E-Mail-Adresse an', mode: 'strict')] #[Assert\Email(message: 'Bitte gib eine gültige E-Mail-Adresse an', mode: Assert\Email::VALIDATION_MODE_STRICT)]
private ?string $email = null; private ?string $email = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
+1 -1
View File
@@ -18,7 +18,7 @@ class Communication
protected ?string $mobile = null; protected ?string $mobile = null;
#[ORM\Column(type: 'string', nullable: true)] #[ORM\Column(type: 'string', nullable: true)]
#[Assert\Email(mode: 'strict', message: 'Bitte gib eine gültige E-Mail-Adresse an', groups: ['profile'])] #[Assert\Email(mode: Assert\Email::VALIDATION_MODE_STRICT, message: 'Bitte gib eine gültige E-Mail-Adresse an', groups: ['profile'])]
#[Assert\NotBlank(message: 'Bitte gib deine E-Mail-Adresse an', groups: ['profile'])] #[Assert\NotBlank(message: 'Bitte gib deine E-Mail-Adresse an', groups: ['profile'])]
protected ?string $email = null; protected ?string $email = null;
+1
View File
@@ -322,6 +322,7 @@ class User implements UserInterface, TimestampableEntityInterface, SoftDeletable
return 'app_teamer_index'; return 'app_teamer_index';
} }
#[\Deprecated]
public function eraseCredentials(): void public function eraseCredentials(): void
{ {
} }
+2 -2
View File
@@ -20,10 +20,10 @@ class UploadSessionListener
$uploadedFile = $event->getFile(); $uploadedFile = $event->getFile();
// Get upload uuid from Dropzone request // Get upload uuid from Dropzone request
$uuid = $request->get('uuid'); $uuid = $request->request->get('uuid');
// Get original filename from Dropzone request // Get original filename from Dropzone request
$originalFileName = $request->get('originalFilename'); $originalFileName = $request->request->get('originalFilename');
$upload = new UploadDto( $upload = new UploadDto(
$uuid, $uuid,
@@ -17,7 +17,7 @@ class UploadSessionValidationListener
$request = $event->getRequest(); $request = $event->getRequest();
$session = $request->getSession(); $session = $request->getSession();
$uploadSessionId = $request->get(UploadHandler::SESSION_KEY); $uploadSessionId = $request->request->get(UploadHandler::SESSION_KEY);
if (null === $uploadSessionId || false === $session->has(UploadHandler::SESSION_KEY)) { if (null === $uploadSessionId || false === $session->has(UploadHandler::SESSION_KEY)) {
throw new ValidationException('Invalid upload session'); throw new ValidationException('Invalid upload session');
+5
View File
@@ -24,6 +24,11 @@ class AbbreviatedDateType extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'input' => 'datetime_immutable', 'input' => 'datetime_immutable',
// Explicit 'choice' preserves the pre-Symfony-7.4 default (7.4 flipped it to
// 'single_text'). This type needs the multi-field widget: the 'years' option at
// the call sites and FirstDayOfMonthDateTransformer both assume array submissions,
// and 'single_text' + the custom 'format' below throws under html5.
'widget' => 'choice',
'format' => 'y-MMMM-d', 'format' => 'y-MMMM-d',
]); ]);
} }
+1 -3
View File
@@ -17,9 +17,7 @@ class DeleteAccountType extends AbstractType
'label' => 'Ja, ich möchte meinen Account endgültig löschen', 'label' => 'Ja, ich möchte meinen Account endgültig löschen',
'mapped' => false, 'mapped' => false,
'constraints' => [ 'constraints' => [
new IsTrue([ new IsTrue(message: 'Bitte bestätige die Löschung'),
'message' => 'Bitte bestätige die Löschung',
]),
], ],
]); ]);
} }
+1 -3
View File
@@ -21,9 +21,7 @@ class DisableUserType extends AbstractType
'data-action' => 'textarea-autosize#resize', 'data-action' => 'textarea-autosize#resize',
], ],
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank(message: 'Bitte gib die Begründung ein'),
'message' => 'Bitte gib die Begründung ein',
]),
], ],
]) ])
->add('disabledReasonInternal', TextareaType::class, [ ->add('disabledReasonInternal', TextareaType::class, [
+1 -3
View File
@@ -30,9 +30,7 @@ class DispositionCallOffType extends AbstractType
'data-action' => 'textarea-autosize#resize', 'data-action' => 'textarea-autosize#resize',
], ],
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank(message: 'Bitte gib die Begründung ein'),
'message' => 'Bitte gib die Begründung ein',
]),
], ],
]) ])
->add('sendNotification', CheckboxType::class, [ ->add('sendNotification', CheckboxType::class, [
+1 -3
View File
@@ -58,9 +58,7 @@ class TeamerApplicationType extends AbstractType
'label' => 'Ich bestätige den abweichenden Buszustieg', 'label' => 'Ich bestätige den abweichenden Buszustieg',
'mapped' => false, 'mapped' => false,
'constraints' => [ 'constraints' => [
new IsTrue([ new IsTrue(message: 'Deine Bestätigung ist erforderlich'),
'message' => 'Deine Bestätigung ist erforderlich',
]),
], ],
]); ]);
} }
+1 -3
View File
@@ -42,9 +42,7 @@ class TeamerDispositionType extends AbstractType
'label' => 'Ich bestätige, dass meine persönlichen Daten, Bankverbindung und Kontoinhaber korrekt sind', 'label' => 'Ich bestätige, dass meine persönlichen Daten, Bankverbindung und Kontoinhaber korrekt sind',
'mapped' => false, 'mapped' => false,
'constraints' => [ 'constraints' => [
new IsTrue([ new IsTrue(message: 'Deine Bestätigung ist erforderlich'),
'message' => 'Deine Bestätigung ist erforderlich',
]),
], ],
]); ]);
} }
+3 -3
View File
@@ -98,8 +98,8 @@ abstract class AbstractMenuBuilder
$request = $this->requestStack->getMainRequest(); $request = $this->requestStack->getMainRequest();
return [ return [
$parameter => $request->get($parameter, $default), $parameter => $request->attributes->get($parameter, $default),
'r' => $request->get('r'), 'r' => $request->query->get('r'),
]; ];
} }
@@ -123,7 +123,7 @@ abstract class AbstractMenuBuilder
$returnUrl = $options['return_url'] ?? null; $returnUrl = $options['return_url'] ?? null;
if (false === is_string($returnUrl) || '' === $returnUrl) { if (false === is_string($returnUrl) || '' === $returnUrl) {
$requestReturnUrl = $this->requestStack->getMainRequest()?->get('r'); $requestReturnUrl = $this->requestStack->getMainRequest()?->query->get('r');
$returnUrl = is_string($requestReturnUrl) && '' !== $requestReturnUrl $returnUrl = is_string($requestReturnUrl) && '' !== $requestReturnUrl
? rawurldecode($requestReturnUrl) ? rawurldecode($requestReturnUrl)
+2 -1
View File
@@ -3,6 +3,7 @@
namespace App\Security; namespace App\Security;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -34,7 +35,7 @@ class UserChecker implements UserCheckerInterface
} }
} }
public function checkPostAuth(UserInterface $user): void public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void
{ {
} }
} }
+2 -1
View File
@@ -6,6 +6,7 @@ use App\Entity\Application;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ApplicationVoter extends Voter class ApplicationVoter extends Voter
@@ -29,7 +30,7 @@ class ApplicationVoter extends Voter
return in_array($attribute, [static::VIEW, static::WITHDRAW, static::DELETE, static::DISPOSE, static::STATUS]); return in_array($attribute, [static::VIEW, static::WITHDRAW, static::DELETE, static::DISPOSE, static::STATUS]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var User $user */ /** @var User $user */
$user = $token->getUser(); $user = $token->getUser();
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\Entity\Assignment; use App\Entity\Assignment;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AssignmentVoter extends Voter class AssignmentVoter extends Voter
@@ -28,7 +29,7 @@ class AssignmentVoter extends Voter
return in_array($attribute, [self::VIEW, self::EDIT, self::APPLY, self::PUBLISH, self::CALL_OFF], true); return in_array($attribute, [self::VIEW, self::EDIT, self::APPLY, self::PUBLISH, self::CALL_OFF], true);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
if (null === $token->getUser()) { if (null === $token->getUser()) {
return false; return false;
+2 -1
View File
@@ -6,6 +6,7 @@ use App\Entity\Availability;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AvailabilityVoter extends Voter class AvailabilityVoter extends Voter
@@ -26,7 +27,7 @@ class AvailabilityVoter extends Voter
return in_array($attribute, [static::ASSIGN, static::DELETE]); return in_array($attribute, [static::ASSIGN, static::DELETE]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var Availability $availability */ /** @var Availability $availability */
$availability = $subject; $availability = $subject;
+2 -1
View File
@@ -7,6 +7,7 @@ use App\Entity\Upload;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class DispositionVoter extends Voter class DispositionVoter extends Voter
@@ -44,7 +45,7 @@ class DispositionVoter extends Voter
]); ]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var Disposition $disposition */ /** @var Disposition $disposition */
$disposition = $subject; $disposition = $subject;
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\Entity\FeedbackSet; use App\Entity\FeedbackSet;
use App\Repository\JobProfileRepository; use App\Repository\JobProfileRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class FeedbackSetVoter extends Voter class FeedbackSetVoter extends Voter
@@ -20,7 +21,7 @@ class FeedbackSetVoter extends Voter
return $subject instanceof FeedbackSet && static::DELETE === $attribute; return $subject instanceof FeedbackSet && static::DELETE === $attribute;
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var FeedbackSet $feedbackSet */ /** @var FeedbackSet $feedbackSet */
$feedbackSet = $subject; $feedbackSet = $subject;
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\Entity\Feedback; use App\Entity\Feedback;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class FeedbackVoter extends Voter class FeedbackVoter extends Voter
@@ -18,7 +19,7 @@ class FeedbackVoter extends Voter
return $subject instanceof Feedback && in_array($attribute, [static::VIEW, static::PUBLISH, static::DELETE]); return $subject instanceof Feedback && in_array($attribute, [static::VIEW, static::PUBLISH, static::DELETE]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var Feedback $feedback */ /** @var Feedback $feedback */
$feedback = $subject; $feedback = $subject;
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -19,7 +20,7 @@ class ImpersonationVoter extends Voter
return 'CAN_IMPERSONATE' === $attribute && $subject instanceof UserInterface; return 'CAN_IMPERSONATE' === $attribute && $subject instanceof UserInterface;
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$currentUser = $token->getUser(); $currentUser = $token->getUser();
$targetUser = $subject; $targetUser = $subject;
+2 -1
View File
@@ -6,6 +6,7 @@ use App\Entity\License;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class LicenseVoter extends Voter class LicenseVoter extends Voter
@@ -26,7 +27,7 @@ class LicenseVoter extends Voter
return in_array($attribute, [static::VIEW, static::DELETE]); return in_array($attribute, [static::VIEW, static::DELETE]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) { if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return true; return true;
+2 -1
View File
@@ -6,6 +6,7 @@ use App\Entity\Upload;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class UploadVoter extends Voter class UploadVoter extends Voter
@@ -29,7 +30,7 @@ class UploadVoter extends Voter
return in_array($attribute, [static::VIEW, static::DELETE, static::DOWNLOAD, static::CHECK, static::COMMENT]); return in_array($attribute, [static::VIEW, static::DELETE, static::DOWNLOAD, static::CHECK, static::COMMENT]);
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
/** @var Upload $upload */ /** @var Upload $upload */
$upload = $subject; $upload = $subject;
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class UserVoter extends Voter class UserVoter extends Voter
@@ -20,7 +21,7 @@ class UserVoter extends Voter
return self::EDIT === $attribute && $subject instanceof User; return self::EDIT === $attribute && $subject instanceof User;
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$currentUser = $token->getUser(); $currentUser = $token->getUser();
$targetUser = $subject; $targetUser = $subject;
+1 -1
View File
@@ -25,7 +25,7 @@ class KnownPlaceholders extends Constraint
*/ */
public function __construct(array $allowed = [], ?string $message = null, ?array $groups = null, mixed $payload = null) public function __construct(array $allowed = [], ?string $message = null, ?array $groups = null, mixed $payload = null)
{ {
parent::__construct([], $groups, $payload); parent::__construct(null, $groups, $payload);
$this->allowed = $allowed; $this->allowed = $allowed;
$this->message = $message ?? $this->message; $this->message = $message ?? $this->message;
+36
View File
@@ -116,6 +116,18 @@
"config/routes/oneup_uploader.yaml" "config/routes/oneup_uploader.yaml"
] ]
}, },
"phpstan/phpstan": {
"version": "2.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
},
"files": [
"phpstan.dist.neon"
]
},
"phpunit/phpunit": { "phpunit/phpunit": {
"version": "9.6", "version": "9.6",
"recipe": { "recipe": {
@@ -178,6 +190,18 @@
".env" ".env"
] ]
}, },
"symfony/form": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": { "symfony/framework-bundle": {
"version": "6.3", "version": "6.3",
"recipe": { "recipe": {
@@ -278,6 +302,18 @@
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"symfony/property-info": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "6.3", "version": "6.3",
"recipe": { "recipe": {
+6 -8
View File
@@ -71,14 +71,12 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block label %} {% block label -%}
{% apply spaceless %} {% if item.extras.icon is defined -%}
{% if item.extras.icon is defined %} {{ icon(item.extras.icon, 'h-5 w-5 shrink-0 text-gray-400') -}}
{{ icon(item.extras.icon, 'h-5 w-5 shrink-0 text-gray-400') }}
<span class="flex-1">{{ item.label|raw }}</span> <span class="flex-1">{{ item.label|raw }}</span>
{% else %} {%- else -%}
{{ item.label|raw }} {{ item.label|raw }}
{% endif %} {%- endif %}
{% endapply %} {%- endblock %}
{% endblock %}
+6 -8
View File
@@ -1,10 +1,8 @@
{% apply spaceless %} {% for message in messages -%}
{% for message in messages %}
{% if message is iterable %} {% if message is iterable %}
{% set message = message.id | trans(message.parameters | default({}), message.domain | default(null), message.locale | default(null)) | raw %} {%- set message = message.id | trans(message.parameters | default({}), message.domain | default(null), message.locale | default(null)) | raw %}
{% else %} {%- else %}
{% set message = message | trans | raw %} {%- set message = message | trans | raw %}
{% endif %} {%- endif -%}
<div {{ stimulus_controller('toast', { 'text': message, 'class': 'toastify--' ~ level}) }}{{ qa_attribute('toast', level)}}></div> <div {{ stimulus_controller('toast', { 'text': message, 'class': 'toastify--' ~ level}) }}{{ qa_attribute('toast', level)}}></div>
{% endfor %} {%- endfor %}
{% endapply %}
-84
View File
@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\Migrations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Yaml;
/**
* Version20260820000000 rewrites the wording of mails that go out to every teamer, and it
* runs exactly once with nobody watching, so the patterns it does that with are pinned
* here. The migration is not autoloaded (only src/ and tests/ are), hence the require.
*/
class EmphasisRewriteTest extends TestCase
{
private string $single;
private string $double;
protected function setUp(): void
{
require_once __DIR__.'/../../migrations/Version20260820000000.php';
$reflection = new \ReflectionClass(\DoctrineMigrations\Version20260820000000::class);
$this->single = $reflection->getConstant('SINGLE_EMPHASIS');
$this->double = $reflection->getConstant('DOUBLE_EMPHASIS');
}
/**
* @dataProvider upCases
*/
public function testTheUpwardRewrite(string $before, string $after): void
{
$this->assertSame($after, preg_replace($this->single, '**$1**', $before));
}
/**
* @return iterable<string, array{string, string}>
*/
public static function upCases(): iterable
{
yield 'one word' => ['Das ist *wichtig*', 'Das ist **wichtig**'];
yield 'several words' => ['*Dein Einsatz wurde angenommen!*', '**Dein Einsatz wurde angenommen!**'];
yield 'around a placeholder' => ['*{destination}*', '**{destination}**'];
yield 'already bold' => ['Das ist **wichtig**', 'Das ist **wichtig**'];
yield 'escaped asterisk' => ['Ein Sternchen: \*', 'Ein Sternchen: \*'];
// Whitespace next to a marker means it is not emphasis - CommonMark's own rule, and
// the reason arithmetic and stray asterisks come through untouched.
yield 'multiplication' => ['5 * 3 = 15', '5 * 3 = 15'];
yield 'spaced asterisks' => ['a * b * c', 'a * b * c'];
yield 'bullet at line start' => ["* Ausweis\n* Bankverbindung", "* Ausweis\n* Bankverbindung"];
yield 'not across lines' => ["*offen\ngeblieben*", "*offen\ngeblieben*"];
}
public function testTheRewriteIsReversible(): void
{
$before = 'Das ist *wichtig* und *{destination}* auch';
$up = preg_replace($this->single, '**$1**', $before);
$this->assertSame($before, preg_replace($this->double, '*$1*', $up));
}
/**
* The delivered defaults were rewritten by hand in the same commit, so nothing should
* be left in them for the migration to find. Read through the parser rather than off
* the raw file: the header comment documents *kursiv* on purpose.
*/
public function testTheDeliveredDefaultsAreAlreadyMigrated(): void
{
$texts = Yaml::parseFile(__DIR__.'/../../config/email_texts.yaml')['texts'];
foreach ($texts as $key => $text) {
$body = (string) ($text['body'] ?? '');
$this->assertSame(
$body,
preg_replace($this->single, '**$1**', $body),
sprintf('Default wording of "%s" still carries single-asterisk emphasis', $key)
);
}
}
}
@@ -21,8 +21,7 @@ class TeamerMailingDraftHandlerTest extends TestCase
$request = new Request(); $request = new Request();
$request->setSession(new Session(new MockArraySessionStorage())); $request->setSession(new Session(new MockArraySessionStorage()));
$requestStack = new RequestStack(); $requestStack = new RequestStack([$request]);
$requestStack->push($request);
$this->handler = new TeamerMailingDraftHandler($requestStack); $this->handler = new TeamerMailingDraftHandler($requestStack);
} }