From 248126189db1764a3c94d3bdc21e1a78146fca6f Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 5 Jan 2025 13:50:00 +0100 Subject: [PATCH 01/12] Increase typing coverage --- app/Components/BootstrapCheckboxList.php | 4 +++- app/Components/BootstrapRadioList.php | 4 +++- app/Components/CategoryEntry.php | 6 +++--- app/Components/ObjectSelectBox.php | 2 +- app/Components/TeamForm.php | 3 ++- app/Forms/TeamFormFactory.php | 2 +- app/Model/Configuration/CategoryData.php | 4 ++-- app/Model/Configuration/CategoryGroup.php | 2 +- .../Configuration/Constraints/Constraint.php | 5 +++++ app/Model/Configuration/Helpers.php | 2 +- app/Presenters/CommunicationPresenter.php | 4 ++-- app/Presenters/TeamPresenter.php | 18 ++++++++++++++++-- .../Filters/WrapInParagraphsFilter.php | 5 ++++- 13 files changed, 44 insertions(+), 17 deletions(-) diff --git a/app/Components/BootstrapCheckboxList.php b/app/Components/BootstrapCheckboxList.php index 989f486..3b52550 100644 --- a/app/Components/BootstrapCheckboxList.php +++ b/app/Components/BootstrapCheckboxList.php @@ -24,11 +24,13 @@ public function getControl(): Html { /** @var Html */ $input = Controls\MultiChoiceControl::getControl(); + $htmlId = $input->id; + \assert(\is_string($htmlId)); $items = $this->getItems(); $ids = []; if ($this->generateId) { foreach ($items as $value => $label) { - $ids[$value] = $input->id . '-' . $value; + $ids[$value] = $htmlId . '-' . $value; } } diff --git a/app/Components/BootstrapRadioList.php b/app/Components/BootstrapRadioList.php index 2cd3319..30d04c8 100644 --- a/app/Components/BootstrapRadioList.php +++ b/app/Components/BootstrapRadioList.php @@ -24,11 +24,13 @@ public function getControl(): Html { /** @var Html */ $input = Controls\ChoiceControl::getControl(); + $htmlId = $input->id; + \assert(\is_string($htmlId)); $items = $this->getItems(); $ids = []; if ($this->generateId) { foreach ($items as $value => $label) { - $ids[$value] = $input->id . '-' . $value; + $ids[$value] = $htmlId . '-' . $value; } } diff --git a/app/Components/CategoryEntry.php b/app/Components/CategoryEntry.php index 4a3d374..c72f60f 100644 --- a/app/Components/CategoryEntry.php +++ b/app/Components/CategoryEntry.php @@ -26,14 +26,14 @@ public function __construct(string $label, Entries $entries, bool $showAll = fal $categoryGroups = $categoryTree; $items = array_combine( array_map( - static fn(CategoryGroup $group) => $group->key, + static fn(CategoryGroup $group): string => $group->key, $categoryGroups, ), array_map( function(CategoryGroup $group) use ($showAll): OptGroup { $categoryArray = array_combine( array_map( - static fn(Category $category) => $category->name, + static fn(Category $category): string => $category->name, $group->categories, ), array_map( @@ -60,7 +60,7 @@ function(CategoryGroup $group) use ($showAll): OptGroup { $categories = $categoryTree; $items = array_combine( array_map( - static fn(Category $category) => $category->name, + static fn(Category $category): string => $category->name, $categories, ), array_map( diff --git a/app/Components/ObjectSelectBox.php b/app/Components/ObjectSelectBox.php index 6b618cb..0ea3b57 100644 --- a/app/Components/ObjectSelectBox.php +++ b/app/Components/ObjectSelectBox.php @@ -39,7 +39,7 @@ public function __construct($label = null, ?array $items = null) { parent::__construct($label, $items); $this->setOption('type', 'select'); $this - ->addCondition(fn() => $this->prompt === false && $this->options && $this->control->size < 2) + ->addCondition(fn(): bool => $this->prompt === false && $this->options && $this->control->size < 2) ->addRule(Nette\Forms\Form::FILLED, Nette\Forms\Validator::$messages[self::VALID]); } diff --git a/app/Components/TeamForm.php b/app/Components/TeamForm.php index 81af43d..ecae100 100644 --- a/app/Components/TeamForm.php +++ b/app/Components/TeamForm.php @@ -19,6 +19,7 @@ use Nette\Forms\Controls\SubmitButton; use Nette\Forms\Rules; use Nette\Localization\Translator; +use Nette\Utils\ArrayHash; use Nette\Utils\Json; use Nextras\FormComponents\Controls\DateControl; use stdClass; @@ -352,7 +353,7 @@ private function checkCategoryConstraints(self $form, stdClass $data): void { $categoryField = $form->getComponent('category'); $constraints = $this->entries->categories->allCategories[$data->category]->constraints; - /** @var iterable> */ + /** @var ArrayHash> */ $persons = $data->persons; foreach ($constraints as $constraint) { if (!$constraint->admits($persons)) { diff --git a/app/Forms/TeamFormFactory.php b/app/Forms/TeamFormFactory.php index 17d65b5..6d86ad0 100644 --- a/app/Forms/TeamFormFactory.php +++ b/app/Forms/TeamFormFactory.php @@ -41,7 +41,7 @@ public function create( $form->setTranslator($this->translator); $renderer = new Bs5FormRenderer(); // We need the class to know what to hide (e.g. for applicableCategories). - $renderer->wrappers['pair']['container'] = preg_replace('(class=")', '$0form-group ', $renderer->wrappers['pair']['container']); + $renderer->wrappers['pair']['container'] = preg_replace('(class=")', '$0form-group ', (string) $renderer->wrappers['pair']['container']); $form->setRenderer($renderer); return $form; diff --git a/app/Model/Configuration/CategoryData.php b/app/Model/Configuration/CategoryData.php index 270af23..a269813 100644 --- a/app/Model/Configuration/CategoryData.php +++ b/app/Model/Configuration/CategoryData.php @@ -71,7 +71,7 @@ public static function from( $groups = $categories; $categoryGroups = array_map( - fn(string $groupKey, array $group) => CategoryGroup::from( + fn(string $groupKey, array $group): CategoryGroup => CategoryGroup::from( $groupKey, Helpers::parseLabel("category group #{$groupKey}", $group, $allLocales), $group, @@ -88,7 +88,7 @@ public static function from( ); } else { $categoriesData = array_map( - fn(string $categoryKey, array $category) => Category::from( + fn(string $categoryKey, array $category): Category => Category::from( $categoryKey, $category, $fees, diff --git a/app/Model/Configuration/CategoryGroup.php b/app/Model/Configuration/CategoryGroup.php index 9607bbb..0cfe04d 100644 --- a/app/Model/Configuration/CategoryGroup.php +++ b/app/Model/Configuration/CategoryGroup.php @@ -36,7 +36,7 @@ public static function from( $categoriesRaw = $group['categories']; $categories = array_map( - fn(string $categoryKey, array $category) => Category::from( + fn(string $categoryKey, array $category): Category => Category::from( $categoryKey, $category, $fees, diff --git a/app/Model/Configuration/Constraints/Constraint.php b/app/Model/Configuration/Constraints/Constraint.php index 7f0219d..5a8ce8c 100644 --- a/app/Model/Configuration/Constraints/Constraint.php +++ b/app/Model/Configuration/Constraints/Constraint.php @@ -7,7 +7,12 @@ namespace App\Model\Configuration\Constraints; +use ArrayAccess; + interface Constraint { + /** + * @param iterable> $members + */ public function admits(iterable $members): bool; public function getErrorMessage(): string; diff --git a/app/Model/Configuration/Helpers.php b/app/Model/Configuration/Helpers.php index ad6c379..a301cc5 100644 --- a/app/Model/Configuration/Helpers.php +++ b/app/Model/Configuration/Helpers.php @@ -228,7 +228,7 @@ public static function makeItems( return array_combine( array_keys($items), array_map( - function(string $name, mixed $item) use ($allLocales, $context, $disabled, $fallbackFee, $fees, $limitName) { + function(string $name, mixed $item) use ($allLocales, $context, $disabled, $fallbackFee, $fees, $limitName): Fields\Item { if (!\is_array($item)) { throw new InvalidConfigurationException("Item {$name} inside {$context} must be an array."); } diff --git a/app/Presenters/CommunicationPresenter.php b/app/Presenters/CommunicationPresenter.php index a8d0f42..3f81e8a 100644 --- a/app/Presenters/CommunicationPresenter.php +++ b/app/Presenters/CommunicationPresenter.php @@ -99,7 +99,7 @@ private function composeFormPreview(SubmitButton $button): void { /** @var array */ // actually \ArrayAccess but PHPStan does not handle that very well. $values = $form->getValues(); - $teamsIds = explode(',', $values['recipients']); + $teamsIds = explode(',', (string) $values['recipients']); $teamsIds = array_map( trim(...), $teamsIds, @@ -181,7 +181,7 @@ private function composeFormEnqueue(SubmitButton $button): void { $values = $form->getValues(); $subject = $values['subject']; - $teamsIds = explode(',', $values['recipients']); + $teamsIds = explode(',', (string) $values['recipients']); $teamsIds = array_map( trim(...), $teamsIds, diff --git a/app/Presenters/TeamPresenter.php b/app/Presenters/TeamPresenter.php index 0517459..76b7822 100644 --- a/app/Presenters/TeamPresenter.php +++ b/app/Presenters/TeamPresenter.php @@ -820,11 +820,11 @@ public function createComponentTeamListActionForm(): Form { private function listActionSubmitMessage(Controls\SubmitButton $button): void { $values = $button->form->getValues(); $selectedTeamIds = array_map( - fn($name) => substr($name, \strlen('team_')), + fn($name): string => substr((string) $name, \strlen('team_')), array_keys( array_filter( (array) $values, - fn($value, $name) => str_starts_with($name, 'team_') && \is_bool($value) && $value, + fn($value, $name): bool => str_starts_with((string) $name, 'team_') && \is_bool($value) && $value, \ARRAY_FILTER_USE_BOTH ) ) @@ -837,18 +837,29 @@ private function listActionSubmitMessage(Controls\SubmitButton $button): void { } } + /** + * @return list + */ private function personData(stdClass $data): array { $fields = $this->entries->personFields; return $this->formatData($data, $fields); } + /** + * @return list + */ private function teamData(stdClass $data): array { $fields = $this->entries->teamFields; return $this->formatData($data, $fields); } + /** + * @param array $fields + * + * @return list + */ private function formatData(stdClass $data, array $fields): array { $ret = []; foreach ($fields as $field) { @@ -891,6 +902,9 @@ private function formatData(stdClass $data, array $fields): array { return $ret; } + /** + * @param array{scope?: string, type?: string, key?: string, value?: string} $item + */ public static function serializeInvoiceItem(array $item): string { $parts = [$item['scope'] ?? '', $item['type'] ?? '', $item['key'] ?? '', $item['value'] ?? '']; diff --git a/app/Templates/Filters/WrapInParagraphsFilter.php b/app/Templates/Filters/WrapInParagraphsFilter.php index 698d7ef..2030b11 100644 --- a/app/Templates/Filters/WrapInParagraphsFilter.php +++ b/app/Templates/Filters/WrapInParagraphsFilter.php @@ -5,11 +5,14 @@ namespace App\Templates\Filters; final class WrapInParagraphsFilter { + /** + * @param list $arr + */ public function __invoke(array $arr): string { return implode( '', array_map( - fn($e) => '

' . $e . '

', + fn($e): string => '

' . $e . '

', $arr ) ); From e909981c19297c06f0fd4d511cefd420d9a3b556 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 01:12:47 +0200 Subject: [PATCH 02/12] Teams: Fix formatting unknown enum values This regressed in 290239ae4c03f88fc321fe55374da313be2b5862. --- app/Presenters/TeamPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Presenters/TeamPresenter.php b/app/Presenters/TeamPresenter.php index 76b7822..c4f394b 100644 --- a/app/Presenters/TeamPresenter.php +++ b/app/Presenters/TeamPresenter.php @@ -883,7 +883,7 @@ private function formatData(stdClass $data, array $fields): array { $ret[] = (string) Html::el('span', ['class' => 'fi fi-' . $country->codeAlpha2]) . ' ' . $country->name; continue; } elseif ($field instanceof Fields\EnumField && isset($data->$name) && isset($field->options[$data->$name])) { - $selectedOption = \array_key_exists($data->$name, $field->options) ? $this->translator->translate($field->options[$data->$name]->label) : $data->name; + $selectedOption = \array_key_exists($data->$name, $field->options) ? $this->translator->translate($field->options[$data->$name]->label) : $data->$name; $ret[] = $label . ': ' . $selectedOption; continue; } elseif ($field instanceof Fields\CheckboxlistField && isset($data->$name)) { From 908b457e19ab5a922897c161e40882bad59214bd Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 01:22:46 +0200 Subject: [PATCH 03/12] TeamPresenter::formatData: Simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `$value` instead of `$data->$name` everywhere. Makes the code slightly easier to read. There is slight change in behaviour: if the data does not have the `Sportident` or `Country` field, it will be skipped, whereas, previously, it would have been listed as “to rent” or “unknown”, respectively. But this could only occur when the configuration changed to add those fields after a team had already been registered so we do not really care. --- app/Presenters/TeamPresenter.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/Presenters/TeamPresenter.php b/app/Presenters/TeamPresenter.php index c4f394b..a705113 100644 --- a/app/Presenters/TeamPresenter.php +++ b/app/Presenters/TeamPresenter.php @@ -870,33 +870,38 @@ private function formatData(stdClass $data, array $fields): array { continue; } + if (!isset($data->$name)) { + continue; + } + + $value = $data->$name; + if ($field instanceof Fields\SportidentField) { - $value = $data->$name->{SportidentControl::NAME_CARD_ID} ?? $this->translator->translate('messages.team.person.si.rent'); + $value = $value->{SportidentControl::NAME_CARD_ID} ?? $this->translator->translate('messages.team.person.si.rent'); $ret[] = $label . ' ' . $value; continue; } elseif ($field instanceof Fields\CountryField) { - $country = isset($data->$name) ? $this->countries->getById($data->$name) : null; + $country = $this->countries->getById($value); if ($country === null) { $ret[] = $this->translator->translate('messages.team.data.country.unknown'); continue; } $ret[] = (string) Html::el('span', ['class' => 'fi fi-' . $country->codeAlpha2]) . ' ' . $country->name; continue; - } elseif ($field instanceof Fields\EnumField && isset($data->$name) && isset($field->options[$data->$name])) { - $selectedOption = \array_key_exists($data->$name, $field->options) ? $this->translator->translate($field->options[$data->$name]->label) : $data->$name; + } elseif ($field instanceof Fields\EnumField && isset($field->options[$value])) { + $selectedOption = \array_key_exists($value, $field->options) ? $this->translator->translate($field->options[$value]->label) : $value; $ret[] = $label . ': ' . $selectedOption; continue; - } elseif ($field instanceof Fields\CheckboxlistField && isset($data->$name)) { + } elseif ($field instanceof Fields\CheckboxlistField) { $items = array_map( fn(string $item): string => isset($field->items[$item]) ? $this->translator->translate($field->items[$item]->label) : $item, - $data->$name + $value ); $ret[] = $label . ' ' . implode(', ', $items); continue; } - if (isset($data->$name)) { - $ret[] = $label . ' ' . $data->$name; - } + + $ret[] = $label . ' ' . $value; } return $ret; From 85a2f31dc324bb8ed5087b543f0ccae5697058e6 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 01:46:02 +0200 Subject: [PATCH 04/12] www/.htaccess: Update to latest web-project https://github.com/nette/web-project/blob/09e5da4bfbc17c199dd7974e6cc222a3854e08bc/www/.htaccess --- www/.htaccess | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/www/.htaccess b/www/.htaccess index a91446c..199409c 100644 --- a/www/.htaccess +++ b/www/.htaccess @@ -1,33 +1,41 @@ # Apache configuration file (see https://httpd.apache.org/docs/current/mod/quickreference.html) + +# Allow access to all resources by default Require all granted -# disable directory listing +# Disable directory listing for security reasons Options -Indexes -# enable cool URL +# Enable pretty URLs (removing the need for "index.php" in the URL) RewriteEngine On + + # Uncomment the next line if you want to set the base URL for rewrites # RewriteBase / - # use HTTPS + # Force usage of HTTPS (secure connection). Uncomment if you have SSL setup. # RewriteCond %{HTTPS} !on # RewriteRule .? https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] - # prevents files starting with dot to be viewed by browser - RewriteCond %{REQUEST_FILENAME} -f - RewriteRule /\.|^\.(?!well-known/) - [F] + # Permit requests to the '.well-known' directory (used for SSL verification and more) + RewriteRule ^\.well-known/.* - [L] + + # Block access to hidden files (starting with a dot) and URLs resembling WordPress admin paths + RewriteRule /\.|^\.|^wp-(login|admin|includes|content) - [F] + + # Return 404 for missing files with specific extensions (images, scripts, styles, archives) + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule \.(pdf|js|mjs|ico|gif|jpg|jpeg|png|webp|avif|svg|css|rar|zip|7z|tar\.gz|map|eot|ttf|otf|woff|woff2)$ - [L] - # front controller + # Front controller pattern - all requests are routed through index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule !\.(pdf|js|ico|gif|jpg|jpeg|png|webp|svg|css|rar|zip|7z|tar\.gz|map|eot|ttf|otf|woff|woff2)$ index.php [L] + RewriteRule . index.php [L] -# enable gzip compression +# Enable gzip compression for text files - - AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json application/xml image/svg+xml - + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json application/xml application/rss+xml image/svg+xml From 2c40ffcd27db8b3dc94c308ab4e8e59064faab11 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 01:50:29 +0200 Subject: [PATCH 05/12] Bootstrap: Add secret cookie Following https://github.com/nette/web-project/commit/9dc365ace46a581e5064e6761c08adf72f4400af `secret` represents a secret key that should be set to `nette-debug` cookie to enable the development mode, in addition to the IP requirement: https://doc.nette.org/en/application/bootstrapping#toc-development-vs-production-mode --- app/Bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Bootstrap.php b/app/Bootstrap.php index c3a2915..4357fe6 100644 --- a/app/Bootstrap.php +++ b/app/Bootstrap.php @@ -9,7 +9,7 @@ final class Bootstrap { public static function boot(): Configurator { $configurator = new Configurator(); - // $configurator->setDebugMode('23.75.345.200'); // enable for your remote IP + // $configurator->setDebugMode('secret@23.75.345.200'); // enable for your remote IP $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTimeZone('UTC'); $configurator->setTempDirectory(__DIR__ . '/../temp'); From 5caf63d66dbc5df2e5ef9f534ac266028229344e Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 02:30:31 +0200 Subject: [PATCH 06/12] Model/InvoiceItem: Rename updater methods They look like setters but the object is actually immutable. --- app/Config/CustomInvoiceModifier.php | 10 +++++----- app/Model/Orm/Invoice/Invoice.php | 2 +- app/Model/Orm/Invoice/InvoiceItem.php | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Config/CustomInvoiceModifier.php b/app/Config/CustomInvoiceModifier.php index 390c923..ff757b8 100644 --- a/app/Config/CustomInvoiceModifier.php +++ b/app/Config/CustomInvoiceModifier.php @@ -49,25 +49,25 @@ private static function fixPersonItemAmounts(Invoice $invoice, int $personCount) $items = $invoice->items; if (isset($items['team:enum:friday2h:yes'])) { - $items['team:enum:friday2h:yes'] = $items['team:enum:friday2h:yes']->setAmount($personCount); + $items['team:enum:friday2h:yes'] = $items['team:enum:friday2h:yes']->withAmount($personCount); } if (isset($items['team:enum:saturday5h:yes'])) { - $items['team:enum:saturday5h:yes'] = $items['team:enum:saturday5h:yes']->setAmount($personCount); + $items['team:enum:saturday5h:yes'] = $items['team:enum:saturday5h:yes']->withAmount($personCount); } if (isset($items['team:enum:sunday4h:yes'])) { - $items['team:enum:sunday4h:yes'] = $items['team:enum:sunday4h:yes']->setAmount($personCount); + $items['team:enum:sunday4h:yes'] = $items['team:enum:sunday4h:yes']->withAmount($personCount); } if (isset($items['all_stages_discount'])) { - $items['all_stages_discount'] = $items['all_stages_discount']->setAmount($personCount); + $items['all_stages_discount'] = $items['all_stages_discount']->withAmount($personCount); } $invoice->items = $items; } private static function discount(InvoiceItem $item, int $discount): InvoiceItem { - return $item->setPrice($item->price->subtract(Money::CZK($discount * 100))); // price in halíř + return $item->withPrice($item->price->subtract(Money::CZK($discount * 100))); // price in halíř } } diff --git a/app/Model/Orm/Invoice/Invoice.php b/app/Model/Orm/Invoice/Invoice.php index 2581d07..a9f900c 100644 --- a/app/Model/Orm/Invoice/Invoice.php +++ b/app/Model/Orm/Invoice/Invoice.php @@ -56,7 +56,7 @@ public function addItem(string $name, ?Money $price = null): self { throw new Exception("Invoice item “{$name}” was not defined."); } - $items[$name] = $items[$name]->addAmount(1); + $items[$name] = $items[$name]->withAmountAdded(1); $this->items = $items; diff --git a/app/Model/Orm/Invoice/InvoiceItem.php b/app/Model/Orm/Invoice/InvoiceItem.php index 8ec6e7d..c49f99c 100644 --- a/app/Model/Orm/Invoice/InvoiceItem.php +++ b/app/Model/Orm/Invoice/InvoiceItem.php @@ -37,16 +37,16 @@ public function getAmount(): int { return $this->amount; } - public function setPrice(Money $price): self { + public function withPrice(Money $price): self { return new self($this->name, $price, $this->amount); } - public function setAmount(int $amount): self { + public function withAmount(int $amount): self { return new self($this->name, $this->price, $amount); } - public function addAmount(int $amount): self { - return $this->setAmount($this->amount + $amount); + public function withAmountAdded(int $amount): self { + return $this->withAmount($this->amount + $amount); } public function jsonSerialize(): array { From 1a219be64687c1cd396269c2f08dcc6fe0965414 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 02:33:16 +0200 Subject: [PATCH 07/12] Model/InvoiceItem: Remove SmartObject Having private members with the same name is quite confusing. Also the properties are readonly so we should have been using `@property-read`. --- app/Config/CustomInvoiceModifier.php | 4 ++-- app/Model/Orm/Invoice/Invoice.php | 4 ++-- app/Model/Orm/Invoice/InvoiceItem.php | 13 +++---------- app/Templates/Invoice/show.latte | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/Config/CustomInvoiceModifier.php b/app/Config/CustomInvoiceModifier.php index ff757b8..220d618 100644 --- a/app/Config/CustomInvoiceModifier.php +++ b/app/Config/CustomInvoiceModifier.php @@ -21,7 +21,7 @@ public static function modify(Team $team, Invoice $invoice, Entries $entries): v $data = $team->getJsonData(); if ($data->friday2h === 'yes' && $data->saturday5h === 'yes' && $data->sunday4h === 'yes') { - $invoice->addItem('all_stages_discount', $invoice->items['team:enum:friday2h:yes']->price->multiply(-1)); + $invoice->addItem('all_stages_discount', $invoice->items['team:enum:friday2h:yes']->getPrice()->multiply(-1)); } self::fixPersonItemAmounts($invoice, \count($team->persons)); @@ -68,6 +68,6 @@ private static function fixPersonItemAmounts(Invoice $invoice, int $personCount) } private static function discount(InvoiceItem $item, int $discount): InvoiceItem { - return $item->withPrice($item->price->subtract(Money::CZK($discount * 100))); // price in halíř + return $item->withPrice($item->getPrice()->subtract(Money::CZK($discount * 100))); // price in halíř } } diff --git a/app/Model/Orm/Invoice/Invoice.php b/app/Model/Orm/Invoice/Invoice.php index a9f900c..246b20c 100644 --- a/app/Model/Orm/Invoice/Invoice.php +++ b/app/Model/Orm/Invoice/Invoice.php @@ -30,7 +30,7 @@ public function createItem(string $name, Money $price): self { $items = $this->items; if (isset($items[$name])) { - $existingPrice = $items[$name]->price; + $existingPrice = $items[$name]->getPrice(); if (!$price->equals($existingPrice)) { throw new Exception("This invoice item “{$name}” already exists with a different price."); } @@ -80,7 +80,7 @@ public function getTotal(?array $filter = null): Money { return Money::sum( ...array_values( array_map( - fn(InvoiceItem $item): Money => $item->price->multiply($item->amount), + fn(InvoiceItem $item): Money => $item->getPrice()->multiply($item->getAmount()), $relevantItems ) ) diff --git a/app/Model/Orm/Invoice/InvoiceItem.php b/app/Model/Orm/Invoice/InvoiceItem.php index c49f99c..63bdc05 100644 --- a/app/Model/Orm/Invoice/InvoiceItem.php +++ b/app/Model/Orm/Invoice/InvoiceItem.php @@ -6,18 +6,11 @@ use JsonSerializable; use Money\Money; -use Nette; /** * Immutable invoice item. - * - * @property string $name - * @property Money $price - * @property int $amount */ final class InvoiceItem implements JsonSerializable { - use Nette\SmartObject; - public function __construct( private readonly string $name, private readonly Money $price, @@ -51,9 +44,9 @@ public function withAmountAdded(int $amount): self { public function jsonSerialize(): array { return [ - 'name' => $this->name, - 'price' => $this->price, - 'amount' => $this->amount, + 'name' => $this->getName(), + 'price' => $this->getPrice(), + 'amount' => $this->getAmount(), ]; } } diff --git a/app/Templates/Invoice/show.latte b/app/Templates/Invoice/show.latte index 7eb8334..55a5e41 100644 --- a/app/Templates/Invoice/show.latte +++ b/app/Templates/Invoice/show.latte @@ -11,7 +11,7 @@ {_messages.billing.invoice.item}{_messages.billing.invoice.unit_price}{_messages.billing.invoice.quantity}{_messages.billing.invoice.price} -{$name|itemLabel}{$item->price|price}{$item->amount}{$item->price->multiply($item->amount)|price} +{$name|itemLabel}{$item->getPrice()|price}{$item->getAmount()}{$item->getPrice()->multiply($item->getAmount())|price} {_messages.billing.invoice.total_price}{$invoice->getTotal()|price} From 20b7a97f4946784dfdd27a3094edbd728b70a7b5 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 02:39:11 +0200 Subject: [PATCH 08/12] Model/Invoice: Declare items type --- app/Model/Orm/Invoice/Invoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Model/Orm/Invoice/Invoice.php b/app/Model/Orm/Invoice/Invoice.php index 246b20c..d86a378 100644 --- a/app/Model/Orm/Invoice/Invoice.php +++ b/app/Model/Orm/Invoice/Invoice.php @@ -17,7 +17,7 @@ * @property string $status {default self::STATUS_NEW} {enum self::STATUS_*} * @property DateTimeImmutable $timestamp {default now} * @property Team $team {m:1 Team::$invoices} - * @property array $items + * @property InvoiceItem[] $items * * @phpstan-property self::STATUS_* $status */ From 32cfdd1d8cb223837d4a0ab667f62581f1c1d889 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 02:39:53 +0200 Subject: [PATCH 09/12] Remove SmartObject Inspired by https://github.com/nette/web-project/commit/38496dc04727d87325e93a8fb59d9ea9d287fd9d --- app/Forms/FormFactory.php | 3 --- app/Forms/TeamFormFactory.php | 3 --- app/Model/TeamManager.php | 2 -- app/Presenters/ErrorPresenter.php | 2 -- 4 files changed, 10 deletions(-) diff --git a/app/Forms/FormFactory.php b/app/Forms/FormFactory.php index e9f59bd..45db2a7 100644 --- a/app/Forms/FormFactory.php +++ b/app/Forms/FormFactory.php @@ -4,15 +4,12 @@ namespace App\Forms; -use Nette; use Nette\Application\UI\Form; use Nette\Localization\Translator; use Nextras\FormsRendering\Renderers\Bs5FormRenderer; use Nextras\FormsRendering\Renderers\FormLayout; final class FormFactory { - use Nette\SmartObject; - public function __construct( private readonly Translator $translator, ) { diff --git a/app/Forms/TeamFormFactory.php b/app/Forms/TeamFormFactory.php index 6d86ad0..9da57dd 100644 --- a/app/Forms/TeamFormFactory.php +++ b/app/Forms/TeamFormFactory.php @@ -6,13 +6,10 @@ use App\Components\TeamForm; use App\Model\Configuration\Entries; -use Nette; use Nette\Localization\Translator; use Nextras\FormsRendering\Renderers\Bs5FormRenderer; final class TeamFormFactory { - use Nette\SmartObject; - public function __construct( private readonly Translator $translator, private readonly Entries $entries, diff --git a/app/Model/TeamManager.php b/app/Model/TeamManager.php index 91a87de..fe4d99d 100644 --- a/app/Model/TeamManager.php +++ b/app/Model/TeamManager.php @@ -13,8 +13,6 @@ use Nette\Security\SimpleIdentity; final class TeamManager implements Nette\Security\Authenticator { - use Nette\SmartObject; - public const ENTRY_WITHDRAWN = 317806432; public function __construct( diff --git a/app/Presenters/ErrorPresenter.php b/app/Presenters/ErrorPresenter.php index 67d33d3..b244e49 100644 --- a/app/Presenters/ErrorPresenter.php +++ b/app/Presenters/ErrorPresenter.php @@ -11,8 +11,6 @@ use Tracy\ILogger; final class ErrorPresenter implements Nette\Application\IPresenter { - use Nette\SmartObject; - public function __construct( private readonly ILogger $logger, ) { From 333a3b833d39f9f928b9b5301bcc76cd4c2c1f21 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 03:26:10 +0200 Subject: [PATCH 10/12] config: Enable strict Latte and use extension to define global filters Based on https://github.com/nette/web-project/commit/44fe1d48b162ef5911012ba1d33c79e1deac4818 and https://github.com/nette/web-project/commit/df257f4fb1e29cffd9c58bca5f26cf4731250ab6 --- app/Config/common.neon | 12 +++++----- app/Templates/Accessory/LatteExtension.php | 27 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/Templates/Accessory/LatteExtension.php diff --git a/app/Config/common.neon b/app/Config/common.neon index 3c60bf2..d7e5356 100644 --- a/app/Config/common.neon +++ b/app/Config/common.neon @@ -27,6 +27,12 @@ di: - Contributte\Translation\LocalesResolvers\Session - Nette\Localization\Translator +latte: + strictTypes: true + strictParsing: true + extensions: + - App\Templates\Accessory\LatteExtension + session: autoStart: yes expiration: 31 days @@ -66,12 +72,6 @@ services: - App\Templates\Filters\PriceFilter - App\Templates\Filters\WrapInParagraphsFilter exchange: Money\Exchange\FixedExchange([]) - nette.latteFactory: - setup: - - addFilter(categoryFormat, @App\Templates\Filters\CategoryFormatFilter) - - addFilter(exchangeCurrency, @App\Templates\Filters\CurrencyExchangeFilter) - - addFilter(price, @App\Templates\Filters\PriceFilter) - - addFilter(wrapInParagraphs, @App\Templates\Filters\WrapInParagraphsFilter) translation: translatorFactory: App\Locale\Translator diff --git a/app/Templates/Accessory/LatteExtension.php b/app/Templates/Accessory/LatteExtension.php new file mode 100644 index 0000000..b41cd8a --- /dev/null +++ b/app/Templates/Accessory/LatteExtension.php @@ -0,0 +1,27 @@ + $this->categoryFormatFilter, + 'exchangeCurrency' => $this->currencyExchangeFilter, + 'price' => $this->priceFilter, + 'wrapInParagraphs' => $this->wrapInParagraphsFilter, + ]; + } +} From 52aa0d0fb27cffd4bad235ab84cd5e6303f8b925 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 03:41:38 +0200 Subject: [PATCH 11/12] ErrorPresenter: Use 4xx template for 405 and add comments Based on https://github.com/nette/web-project/commit/14b830fe4ff1c45ebb4f6f0d6074626ec752d58f Also include comments from https://github.com/nette/web-project/commit/c3541408ff16c826d74c3dd35eab83783d4b37e6 --- app/Presenters/Error4xxPresenter.php | 15 ++++++++++----- app/Presenters/ErrorPresenter.php | 9 ++++++++- app/Templates/Error/405.latte | 6 ------ app/Templates/Error/4xx.latte | 2 ++ 4 files changed, 20 insertions(+), 12 deletions(-) delete mode 100644 app/Templates/Error/405.latte diff --git a/app/Presenters/Error4xxPresenter.php b/app/Presenters/Error4xxPresenter.php index 14a0df3..c62e577 100644 --- a/app/Presenters/Error4xxPresenter.php +++ b/app/Presenters/Error4xxPresenter.php @@ -7,20 +7,25 @@ use Nette; /** + * Handles 4xx HTTP error responses. + * * @property Nette\Application\UI\Template $template */ final class Error4xxPresenter extends BasePresenter { - public function startup(): void { - parent::startup(); + protected function checkHttpMethod(): void { + // allow access via all HTTP methods and ensure the request is a forward (internal redirect) if ($this->getRequest() === null || !$this->getRequest()->isMethod(Nette\Application\Request::FORWARD)) { $this->error(); } } public function renderDefault(Nette\Application\BadRequestException $exception): void { - // load template 403.latte or 404.latte or ... 4xx.latte - $file = __DIR__ . "/../Templates/Error/{$exception->getCode()}.latte"; - $file = is_file($file) ? $file : __DIR__ . '/../Templates/Error/4xx.latte'; + // renders the appropriate error template based on the HTTP status code + $code = $exception->getCode(); + $file = is_file($file = __DIR__ . "/../Templates/Error/$code.latte") + ? $file + : __DIR__ . '/../Templates/Error/4xx.latte'; + $this->template->httpCode = $code; $this->template->setFile($file); } } diff --git a/app/Presenters/ErrorPresenter.php b/app/Presenters/ErrorPresenter.php index b244e49..893dde9 100644 --- a/app/Presenters/ErrorPresenter.php +++ b/app/Presenters/ErrorPresenter.php @@ -10,6 +10,9 @@ use Nette\Http; use Tracy\ILogger; +/** + * Handles uncaught exceptions and errors, and logs them. + */ final class ErrorPresenter implements Nette\Application\IPresenter { public function __construct( private readonly ILogger $logger, @@ -24,13 +27,17 @@ public function run(Nette\Application\Request $request): Nette\Application\Respo $errorPresenter = $module . $sep . 'ErrorAccess'; return new Responses\ForwardResponse($request->setPresenterName($errorPresenter)); - } elseif ($exception instanceof Nette\Application\BadRequestException) { + } + + // If the exception is a 4xx HTTP error, forward to the Error4xxPresenter + if ($exception instanceof Nette\Application\BadRequestException) { [$module, , $sep] = Nette\Application\Helpers::splitName($request->getPresenterName()); $errorPresenter = $module . $sep . 'Error4xx'; return new Responses\ForwardResponse($request->setPresenterName($errorPresenter)); } + // Log the exception and display a generic error message to the user $this->logger->log($exception, ILogger::EXCEPTION); return new Responses\CallbackResponse(function(Http\IRequest $httpRequest, Http\IResponse $httpResponse): void { diff --git a/app/Templates/Error/405.latte b/app/Templates/Error/405.latte deleted file mode 100644 index f5b5df0..0000000 --- a/app/Templates/Error/405.latte +++ /dev/null @@ -1,6 +0,0 @@ -{block content} -

Method Not Allowed

- -

The requested method is not allowed for the URL.

- -

error 405

diff --git a/app/Templates/Error/4xx.latte b/app/Templates/Error/4xx.latte index 3b9f5b0..c6ec0eb 100644 --- a/app/Templates/Error/4xx.latte +++ b/app/Templates/Error/4xx.latte @@ -2,3 +2,5 @@

Oops…

Your browser sent a request that this server could not understand or process.

+ +

error {$httpCode}

From d9e5b41e4be40ac8dae469ecb8336d7dfeeacfa2 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 8 Jun 2025 03:47:03 +0200 Subject: [PATCH 12/12] Error4xxPresenter: Avoid duplicating title The layout template already includes the title in the body. --- app/Templates/Error/403.latte | 2 +- app/Templates/Error/404.latte | 2 +- app/Templates/Error/410.latte | 2 +- app/Templates/Error/4xx.latte | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Templates/Error/403.latte b/app/Templates/Error/403.latte index 6a13674..44e1e47 100644 --- a/app/Templates/Error/403.latte +++ b/app/Templates/Error/403.latte @@ -1,5 +1,5 @@ {block content} -

Access Denied

+{define title}Access Denied{/define}

You do not have permission to view this page. Please try contact the web site administrator if you believe you should be able to view this page.

diff --git a/app/Templates/Error/404.latte b/app/Templates/Error/404.latte index 67ede7f..da51078 100644 --- a/app/Templates/Error/404.latte +++ b/app/Templates/Error/404.latte @@ -1,5 +1,5 @@ {block content} -

Page Not Found

+{define title}Page Not Found{/define}

The page you requested could not be found. It is possible that the address is incorrect, or that the page no longer exists. Please use a search engine to find diff --git a/app/Templates/Error/410.latte b/app/Templates/Error/410.latte index f391aa3..192e0c8 100644 --- a/app/Templates/Error/410.latte +++ b/app/Templates/Error/410.latte @@ -1,5 +1,5 @@ {block content} -

Page Not Found

+{define title}Page Not Found{/define}

The page you requested has been taken off the site. We apologize for the inconvenience.

diff --git a/app/Templates/Error/4xx.latte b/app/Templates/Error/4xx.latte index c6ec0eb..c2551b3 100644 --- a/app/Templates/Error/4xx.latte +++ b/app/Templates/Error/4xx.latte @@ -1,5 +1,5 @@ {block content} -

Oops…

+{define title}Oops…{/define}

Your browser sent a request that this server could not understand or process.