feat(main): New module init

This commit is contained in:
2026-07-09 15:41:40 +02:00
parent 8424ad149e
commit d1d10cebba
41 changed files with 2184 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
vendor/
+75
View File
@@ -0,0 +1,75 @@
# ws_customerspecialty
PrestaShop module (1.7 - 9.0 compatible) that adds a manageable "Specialty" field to customer
accounts.
## What it does
- Back office: a **Customer Specialties** admin screen (under Customers) lets you maintain the
list of selectable specialty values (title per language, position, active/inactive). Values
are never hard-deleted from the UI, only deactivated, since they may already be linked to
customers.
- Back office: the native "Add/Edit customer" page gets a required "Specialty" dropdown, and the
native Customers grid gets a sortable/filterable "Specialty" column and a read-only specialty
block on the customer view page.
- Front office: the registration / "my account" form gets a required "Specialty" dropdown, gated
by a **Configure** toggle (`WS_CUSTOMERSPECIALTY_FRONT_ENABLED`) so it can be hidden from the
storefront without affecting back-office management.
- The customer's specialty title is exposed as a `{specialization}` order confirmation mail
variable (see below).
## Data model
The customer's specialty is stored in the module's own tables — `ws_customerspecialty`,
`ws_customerspecialty_lang`, and the pivot table `ws_customerspecialty_customer` — rather than by
altering `ps_customer` or overriding core classes. This keeps the module safe to run unchanged
across PrestaShop 1.7 through 9.0, since it never depends on core method internals that vary
between major versions.
## Order confirmation email integration
The module does **not** ship its own copy of the order confirmation mail template. Instead it
exposes the customer's specialty as `{specialization}` via the `actionGetExtraMailTemplateVars`
hook (the same pattern used in `ws_giftdropdown`). To show it in an order confirmation email, add
the placeholder to the shop's own `order_conf.html` / `order_conf.txt` mail templates, e.g.:
```
Specialty: {specialization}
```
If the placeholder isn't present in the active theme's mail template, the variable is computed
but simply has nothing to render into — this is a one-time manual integration step per shop/theme,
not something the module can safely do on your behalf without overriding core mail templates.
## Compatibility notes
- Command handlers are tagged for both `tactician.handler` (PS 1.7/8) and `messenger.message_handler`
(PS 9), with matching `__invoke()`/`handle()` methods on each handler.
- No PrestaShop core class overrides, no `ALTER TABLE` on core tables.
## Deliberate differences from the legacy `pchcustomerfield` module
- No drag-and-drop reordering of specialty values. The grid has "Move up" / "Move down" row
actions instead (`customerspecialty_move_up` / `_move_down` routes swapping `position` with the
adjacent row). This avoids depending on PrestaShop's core Grid position/drag-reorder plumbing,
whose exact API wasn't verified live against a running 1.7 and 9.0 instance.
- The active/inactive toggle uses a `ToggleColumn` + `customerspecialty_toggle_active` route. If
that route ever misbehaves on a given PrestaShop version, active/inactive can always be changed
via the Edit form as a fallback — it isn't the only way to flip the flag.
- Grid `getFilters()`/`getColumns()` option keys (`ToggleColumn`, `SearchAndResetType`) follow the
standard, long-stable PrestaShop Grid API from memory; they were not verified against a live
PrestaShop 1.7 or 9.0 install (per request, devdocs research was skipped for this build). Smoke
test the "Customer Specialties" admin grid after install before relying on it in production.
## Biggest assumption to verify
The storefront "Specialty" field on registration/account-edit relies on `additionalCustomerFormFields`
already being called by core's own front-office `CustomerFormatter` (passing `fields` by reference).
The legacy module achieved this only by overriding `CustomerFormatter` to add that hook call itself —
an override intentionally not reproduced here (see the architecture note above). If the installed
PrestaShop version's `CustomerFormatter` does not already call this hook natively, the storefront
field will not appear (back office, grid, and mail-variable behavior are unaffected either way,
since none of those depend on this hook). Verify on a real 1.7 and 9.0 install; if the hook isn't
native, the fallback is to make the field mandatory server-side via a small first-party
`CustomerFormatter` override scoped to only adding that one `Hook::exec()` call — much smaller and
safer than the legacy module's full-method copy.
+25
View File
@@ -0,0 +1,25 @@
{
"name": "websmid/customerspecialty",
"description": "Adds a manageable specialty/profession field to customer accounts",
"authors": [
{
"name": "Isabelle Oving-Anno - De Websmid BV",
"email": "isabelle@dewebsmid.nl"
}
],
"require": {},
"autoload": {
"psr-4": {
"Websmid\\CustomerSpecialty\\": "src/"
},
"classmap": ["ws_customerspecialty.php"],
"exclude-from-classmap": []
},
"config": {
"preferred-install": "dist",
"prepend-autoloader": false
},
"type": "prestashop-module",
"author": "Isabelle Oving-Anno - De Websmid BV",
"license": "MIT"
}
+18
View File
@@ -0,0 +1,18 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "523e34cf5681a91b03bd57967a2d35e0",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
+24
View File
@@ -0,0 +1,24 @@
services:
websmid.customerspecialty.command_handler.save_customer_specialty_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\SaveCustomerSpecialtyCommandHandler'
tags:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\SaveCustomerSpecialtyCommand' }
- { name: 'messenger.message_handler' }
websmid.customerspecialty.command_handler.move_customer_specialty_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\MoveCustomerSpecialtyCommandHandler'
tags:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand' }
- { name: 'messenger.message_handler' }
websmid.customerspecialty.command_handler.toggle_customer_specialty_active_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\ToggleCustomerSpecialtyActiveCommandHandler'
tags:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand' }
- { name: 'messenger.message_handler' }
websmid.customerspecialty.command_handler.link_customer_specialty_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\LinkCustomerSpecialtyCommandHandler'
tags:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand' }
- { name: 'messenger.message_handler' }
+36
View File
@@ -0,0 +1,36 @@
services:
websmid.customerspecialty.form.type.customerspecialty:
public: true
class: 'Websmid\CustomerSpecialty\Form\Type\CustomerSpecialtyType'
parent: 'form.type.translatable.aware'
tags:
- { name: form.type }
websmid.customerspecialty.form.customerspecialty_data_provider:
public: true
class: 'Websmid\CustomerSpecialty\Form\DataProvider\CustomerSpecialtyDataProvider'
websmid.customerspecialty.form.customerspecialty_data_form_builder:
public: true
class: 'PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Builder\FormBuilder'
factory:
- '@prestashop.core.form.builder.form_builder_factory'
- 'create'
arguments:
- 'Websmid\CustomerSpecialty\Form\Type\CustomerSpecialtyType'
- '@websmid.customerspecialty.form.customerspecialty_data_provider'
websmid.customerspecialty.form.customerspecialty_data_handler:
public: true
class: 'Websmid\CustomerSpecialty\Form\DataHandler\CustomerSpecialtyDataHandler'
arguments:
- '@prestashop.core.command_bus'
websmid.customerspecialty.form.customerspecialty_form_handler:
public: true
class: 'PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Handler\FormHandler'
factory:
- '@prestashop.core.form.identifiable_object.handler.form_handler_factory'
- 'create'
arguments:
- '@websmid.customerspecialty.form.customerspecialty_data_handler'
+34
View File
@@ -0,0 +1,34 @@
services:
websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory:
public: true
class: 'Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory'
parent: 'prestashop.core.grid.definition.factory.abstract_grid_definition'
arguments:
- '@prestashop.adapter.legacy.configuration'
websmid.customerspecialty.grid.query.customerspecialty_query_builder:
public: true
class: 'Websmid\CustomerSpecialty\Grid\Query\CustomerSpecialtyQueryBuilder'
parent: 'prestashop.core.grid.abstract_query_builder'
arguments:
- '@prestashop.core.query.doctrine_search_criteria_applicator'
- "@=service('prestashop.adapter.legacy.context').getContext().language.id"
- '@prestashop.core.grid.query.filter.doctrine_filter_applicator'
websmid.customerspecialty.grid.data.factory.customerspecialty_data_factory:
public: true
class: '%prestashop.core.grid.data.factory.doctrine_grid_data_factory%'
arguments:
- '@websmid.customerspecialty.grid.query.customerspecialty_query_builder'
- '@prestashop.core.hook.dispatcher'
- '@prestashop.core.grid.query.doctrine_query_parser'
- 'CustomerSpecialty'
websmid.customerspecialty.grid.customerspecialty_grid_factory:
public: true
class: 'PrestaShop\PrestaShop\Core\Grid\GridFactory'
arguments:
- '@websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory'
- '@websmid.customerspecialty.grid.data.factory.customerspecialty_data_factory'
- '@prestashop.core.grid.filter.form_factory'
- '@prestashop.core.hook.dispatcher'
+4
View File
@@ -0,0 +1,4 @@
services:
websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider:
public: true
class: 'Websmid\CustomerSpecialty\Form\ChoiceProvider\CustomerSpecialtyChoiceProvider'
+49
View File
@@ -0,0 +1,49 @@
customerspecialty_index:
path: customerspecialty
methods: [GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::indexAction'
_legacy_controller: AdminCustomerSpecialtyOverview
_legacy_link: AdminCustomerSpecialtyOverview:index
customerspecialty_search:
path: customerspecialty
methods: [POST]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::searchAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_create:
path: customerspecialty/create
methods: [POST, GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::createAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_edit:
path: customerspecialty/edit/{idCustomerSpecialty}
methods: [POST, GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::editAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_toggle_active:
path: customerspecialty/toggle-active/{idCustomerSpecialty}
methods: [GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::toggleActiveAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_move_up:
path: customerspecialty/move-up/{idCustomerSpecialty}
methods: [GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::moveUpAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_move_down:
path: customerspecialty/move-down/{idCustomerSpecialty}
methods: [GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::moveDownAction'
_legacy_controller: AdminCustomerSpecialtyOverview
+13
View File
@@ -0,0 +1,13 @@
imports:
- { resource: form.yml }
- { resource: grid.yml }
- { resource: command.yml }
- { resource: provider.yml }
services:
websmid.customerspecialty.entity.table_prefix_subscriber:
class: 'Websmid\CustomerSpecialty\Entity\TablePrefixSubscriber'
arguments:
- '%database_prefix%'
tags:
- { name: doctrine.event_subscriber }
+8
View File
@@ -0,0 +1,8 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;
@@ -0,0 +1,40 @@
<?php
namespace Websmid\CustomerSpecialty\Command\CommandHandler;
use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialtyCustomer;
use DateTime;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
final class LinkCustomerSpecialtyCommandHandler
{
private $entityManager;
public function __invoke(LinkCustomerSpecialtyCommand $command)
{
return $this->handle($command);
}
public function handle(LinkCustomerSpecialtyCommand $command)
{
$this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$repository = $this->entityManager->getRepository(CustomerSpecialtyCustomer::class);
$link = $repository->findOneBy(['idCustomer' => $command->getIdCustomer()]);
if (!$link) {
$link = new CustomerSpecialtyCustomer();
$link->setIdCustomer($command->getIdCustomer());
$link->setDateAdd(new DateTime());
}
$link->setIdCustomerSpecialty($command->getIdCustomerSpecialty());
$link->setDateUpd(new DateTime());
$this->entityManager->persist($link);
$this->entityManager->flush();
return true;
}
}
@@ -0,0 +1,68 @@
<?php
namespace Websmid\CustomerSpecialty\Command\CommandHandler;
use Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialty;
use Doctrine\ORM\EntityRepository;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShopException;
final class MoveCustomerSpecialtyCommandHandler
{
private $entityManager;
public function __invoke(MoveCustomerSpecialtyCommand $command)
{
return $this->handle($command);
}
public function handle(MoveCustomerSpecialtyCommand $command)
{
$this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$repository = $this->entityManager->getRepository(CustomerSpecialty::class);
$customerSpecialty = $repository->find($command->getIdCustomerSpecialty());
if (!$customerSpecialty) {
throw new PrestaShopException('Customer specialty not found.');
}
$neighbor = $this->findNeighbor($repository, $customerSpecialty, $command->getDirection());
if (!$neighbor) {
return true;
}
$currentPosition = $customerSpecialty->getPosition();
$customerSpecialty->setPosition($neighbor->getPosition());
$neighbor->setPosition($currentPosition);
$this->entityManager->persist($customerSpecialty);
$this->entityManager->persist($neighbor);
$this->entityManager->flush();
return true;
}
private function findNeighbor(EntityRepository $repository, CustomerSpecialty $customerSpecialty, string $direction): ?CustomerSpecialty
{
$queryBuilder = $repository->createQueryBuilder('cs');
if (MoveCustomerSpecialtyCommand::DIRECTION_UP === $direction) {
$queryBuilder
->where('cs.position < :position')
->orderBy('cs.position', 'DESC');
} else {
$queryBuilder
->where('cs.position > :position')
->orderBy('cs.position', 'ASC');
}
return $queryBuilder
->setParameter('position', $customerSpecialty->getPosition())
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
}
@@ -0,0 +1,104 @@
<?php
namespace Websmid\CustomerSpecialty\Command\CommandHandler;
use Websmid\CustomerSpecialty\Command\SaveCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialty;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialtyLang;
use DateTime;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShopException;
final class SaveCustomerSpecialtyCommandHandler
{
private $entityManager;
public function __invoke(SaveCustomerSpecialtyCommand $command)
{
return $this->handle($command);
}
public function handle(SaveCustomerSpecialtyCommand $command)
{
if (empty(array_filter($command->getTitles()))) {
throw new PrestaShopException('At least one title is required.');
}
$this->entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$customerSpecialty = $this->findOrCreateCustomerSpecialty($command->getIdCustomerSpecialty());
$customerSpecialty->setActive($command->isActive());
$this->stampDates($customerSpecialty);
$this->entityManager->persist($customerSpecialty);
$this->entityManager->flush();
$this->saveTitles($customerSpecialty->getId(), $command->getTitles());
return $customerSpecialty->getId();
}
private function findOrCreateCustomerSpecialty(int $idCustomerSpecialty): CustomerSpecialty
{
if ($idCustomerSpecialty > 0) {
$customerSpecialty = $this->entityManager->getRepository(CustomerSpecialty::class)->find($idCustomerSpecialty);
if ($customerSpecialty) {
return $customerSpecialty;
}
}
$customerSpecialty = new CustomerSpecialty();
$customerSpecialty->setPosition($this->getNextPosition());
return $customerSpecialty;
}
private function getNextPosition(): int
{
$result = $this->entityManager->getRepository(CustomerSpecialty::class)
->createQueryBuilder('cs')
->select('MAX(cs.position) as maxPosition')
->getQuery()
->getSingleScalarResult();
return null === $result ? 0 : ((int) $result + 1);
}
private function stampDates(CustomerSpecialty $customerSpecialty): void
{
$now = new DateTime();
if (null === $customerSpecialty->getId()) {
$customerSpecialty->setDateAdd($now);
}
$customerSpecialty->setDateUpd($now);
}
private function saveTitles(int $idCustomerSpecialty, array $titles): void
{
$repository = $this->entityManager->getRepository(CustomerSpecialtyLang::class);
foreach ($repository->findBy(['idCustomerSpecialty' => $idCustomerSpecialty]) as $existingLang) {
$this->entityManager->remove($existingLang);
}
$this->entityManager->flush();
foreach ($titles as $idLang => $title) {
if ('' === trim((string) $title)) {
continue;
}
$lang = new CustomerSpecialtyLang();
$lang->setIdCustomerSpecialty($idCustomerSpecialty);
$lang->setIdLang((int) $idLang);
$lang->setTitle($title);
$this->entityManager->persist($lang);
}
$this->entityManager->flush();
}
}
@@ -0,0 +1,35 @@
<?php
namespace Websmid\CustomerSpecialty\Command\CommandHandler;
use Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialty;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShopException;
final class ToggleCustomerSpecialtyActiveCommandHandler
{
public function __invoke(ToggleCustomerSpecialtyActiveCommand $command)
{
return $this->handle($command);
}
public function handle(ToggleCustomerSpecialtyActiveCommand $command)
{
$entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$repository = $entityManager->getRepository(CustomerSpecialty::class);
$customerSpecialty = $repository->find($command->getIdCustomerSpecialty());
if (!$customerSpecialty) {
throw new PrestaShopException('Customer specialty not found.');
}
$customerSpecialty->setActive(!$customerSpecialty->isActive());
$entityManager->persist($customerSpecialty);
$entityManager->flush();
return true;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Websmid\CustomerSpecialty\Command;
class LinkCustomerSpecialtyCommand
{
private $idCustomer;
private $idCustomerSpecialty;
public function __construct(int $idCustomer, int $idCustomerSpecialty)
{
$this->idCustomer = $idCustomer;
$this->idCustomerSpecialty = $idCustomerSpecialty;
}
public function getIdCustomer(): int
{
return $this->idCustomer;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Websmid\CustomerSpecialty\Command;
class MoveCustomerSpecialtyCommand
{
const DIRECTION_UP = 'up';
const DIRECTION_DOWN = 'down';
private $idCustomerSpecialty;
private $direction;
public function __construct(int $idCustomerSpecialty, string $direction)
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
$this->direction = $direction;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
public function getDirection(): string
{
return $this->direction;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Websmid\CustomerSpecialty\Command;
class SaveCustomerSpecialtyCommand
{
private $idCustomerSpecialty;
private $active;
private $titles;
public function __construct(int $idCustomerSpecialty, bool $active, array $titles)
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
$this->active = $active;
$this->titles = $titles;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
public function isActive(): bool
{
return $this->active;
}
public function getTitles(): array
{
return $this->titles;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Websmid\CustomerSpecialty\Command;
class ToggleCustomerSpecialtyActiveCommand
{
private $idCustomerSpecialty;
public function __construct(int $idCustomerSpecialty)
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Websmid\CustomerSpecialty\Config;
class Config
{
const HOOKS = [
'additionalCustomerFormFields',
'actionCustomerFormBuilderModifier',
'actionAfterCreateCustomerFormHandler',
'actionAfterUpdateCustomerFormHandler',
'actionObjectCustomerAddAfter',
'actionObjectCustomerUpdateAfter',
'actionCustomerGridDefinitionModifier',
'actionCustomerGridQueryBuilderModifier',
'displayAdminCustomers',
'actionGetExtraMailTemplateVars',
];
const TABS = [
[
'class_name' => 'AdminCustomerSpecialtyOverview',
'parent_class_name' => 'AdminParentCustomer',
'name' => [
'en' => 'Customer Specialties',
'nl' => 'Klantspecialisaties',
],
'module' => 'ws_customerspecialty',
'icon' => 'work',
'route_name' => 'customerspecialty_index',
'active' => 1,
],
];
const FRONT_ENABLED_CONFIG_KEY = 'WS_CUSTOMERSPECIALTY_FRONT_ENABLED';
}
@@ -0,0 +1,157 @@
<?php
namespace Websmid\CustomerSpecialty\Controller;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Security\Voter\PageVoter;
use Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory;
use Websmid\CustomerSpecialty\Grid\Filters\CustomerSpecialtyFilters;
use Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use PrestaShopException;
use Exception;
class CustomerSpecialtyController extends FrameworkBundleAdminController
{
public function indexAction(Request $request, CustomerSpecialtyFilters $filters): Response
{
$gridFactory = $this->get('websmid.customerspecialty.grid.customerspecialty_grid_factory');
$grid = $gridFactory->getGrid($filters);
return $this->render('@Modules/ws_customerspecialty/views/templates/admin/grid/customerspecialty.html.twig', [
'customerSpecialtyGrid' => $this->presentGrid($grid),
'layoutHeaderToolbarBtn' => $this->getToolbarButtons($request->get('_legacy_controller')),
]);
}
public function searchAction(Request $request): RedirectResponse
{
return $this->get('prestashop.bundle.grid.response_builder')->buildSearchResponse(
$this->get('websmid.customerspecialty.grid.definition.factory.customerspecialty_definition_factory'),
$request,
CustomerSpecialtyGridDefinitionFactory::GRID_ID,
'customerspecialty_index'
);
}
public function createAction(Request $request): Response
{
$formBuilder = $this->get('websmid.customerspecialty.form.customerspecialty_data_form_builder');
$formHandler = $this->get('websmid.customerspecialty.form.customerspecialty_form_handler');
$form = $formBuilder->getForm();
$form->handleRequest($request);
try {
$handlerResult = $formHandler->handle($form);
if (null !== $handlerResult->getIdentifiableObjectId()) {
$this->addFlash('success', $this->trans('Successful creation', [], 'Admin.Notifications.Success'));
return $this->redirectToRoute('customerspecialty_index');
}
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
}
return $this->render('@Modules/ws_customerspecialty/views/templates/admin/form/customerspecialty.html.twig', [
'customerSpecialtyForm' => $form->createView(),
]);
}
public function editAction(Request $request, int $idCustomerSpecialty): Response
{
$formBuilder = $this->get('websmid.customerspecialty.form.customerspecialty_data_form_builder');
$formHandler = $this->get('websmid.customerspecialty.form.customerspecialty_form_handler');
$form = $formBuilder->getFormFor($idCustomerSpecialty);
try {
$form->handleRequest($request);
$handlerResult = $formHandler->handleFor($idCustomerSpecialty, $form);
if ($handlerResult->isSubmitted() && $handlerResult->isValid()) {
$this->addFlash('success', $this->trans('Successful update', [], 'Admin.Notifications.Success'));
return $this->redirectToRoute('customerspecialty_index');
}
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
}
return $this->render('@Modules/ws_customerspecialty/views/templates/admin/form/customerspecialty.html.twig', [
'customerSpecialtyForm' => $form->createView(),
]);
}
public function toggleActiveAction(int $idCustomerSpecialty): RedirectResponse
{
try {
$this->getCommandBus()->handle(new ToggleCustomerSpecialtyActiveCommand($idCustomerSpecialty));
$this->addFlash('success', $this->trans('Successful update', [], 'Admin.Notifications.Success'));
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
}
return $this->redirectToRoute('customerspecialty_index');
}
public function moveUpAction(int $idCustomerSpecialty): RedirectResponse
{
return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_UP);
}
public function moveDownAction(int $idCustomerSpecialty): RedirectResponse
{
return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_DOWN);
}
private function move(int $idCustomerSpecialty, string $direction): RedirectResponse
{
try {
$this->getCommandBus()->handle(new MoveCustomerSpecialtyCommand($idCustomerSpecialty, $direction));
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
}
return $this->redirectToRoute('customerspecialty_index');
}
private function getToolbarButtons(string $securitySubject): array
{
$toolbarButtons = [];
if (!$this->isGranted(PageVoter::CREATE, $securitySubject)) {
return $toolbarButtons;
}
$toolbarButtons['customerspecialty_create'] = [
'href' => $this->generateUrl('customerspecialty_create'),
'desc' => $this->trans('New specialty', [], 'Admin.Actions'),
'icon' => 'add_circle_outline',
'class' => 'btn-primary',
'floating_class' => '',
];
return $toolbarButtons;
}
private function getErrorMessages(): array
{
return [
PrestaShopException::class => [
'key' => 'An error occurred while saving the customer specialty.',
'domain' => 'Modules.Wscustomerspecialty.Admin',
'parameters' => [],
],
];
}
private function getCommandBus()
{
return $this->get('prestashop.core.command_bus');
}
}
@@ -0,0 +1,104 @@
<?php
namespace Websmid\CustomerSpecialty\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="ws_customerspecialty")
* @ORM\Entity()
*/
class CustomerSpecialty
{
/**
* @ORM\Id
* @ORM\Column(name="id_ws_customerspecialty", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="position", type="integer")
*/
private $position;
/**
* @ORM\Column(name="active", type="boolean")
*/
private $active;
/**
* @ORM\Column(name="date_add", type="datetime")
*/
private $dateAdd;
/**
* @ORM\Column(name="date_upd", type="datetime")
*/
private $dateUpd;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): int
{
return $this->position;
}
public function setPosition(int $position): self
{
$this->position = $position;
return $this;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
public function getDateAdd(): ?DateTime
{
return $this->dateAdd;
}
public function setDateAdd(DateTime $dateAdd): self
{
$this->dateAdd = $dateAdd;
return $this;
}
public function getDateUpd(): ?DateTime
{
return $this->dateUpd;
}
public function setDateUpd(DateTime $dateUpd): self
{
$this->dateUpd = $dateUpd;
return $this;
}
public function toArray(): array
{
return [
'id_ws_customerspecialty' => $this->id,
'position' => $this->position,
'active' => $this->active,
'date_add' => $this->dateAdd ? $this->dateAdd->format('Y-m-d H:i:s') : null,
'date_upd' => $this->dateUpd ? $this->dateUpd->format('Y-m-d H:i:s') : null,
];
}
}
@@ -0,0 +1,104 @@
<?php
namespace Websmid\CustomerSpecialty\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="ws_customerspecialty_customer")
* @ORM\Entity()
*/
class CustomerSpecialtyCustomer
{
/**
* @ORM\Id
* @ORM\Column(name="id_ws_customerspecialty_customer", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="id_customer", type="integer", unique=true)
*/
private $idCustomer;
/**
* @ORM\Column(name="id_ws_customerspecialty", type="integer")
*/
private $idCustomerSpecialty;
/**
* @ORM\Column(name="date_add", type="datetime")
*/
private $dateAdd;
/**
* @ORM\Column(name="date_upd", type="datetime")
*/
private $dateUpd;
public function getId(): ?int
{
return $this->id;
}
public function getIdCustomer(): int
{
return $this->idCustomer;
}
public function setIdCustomer(int $idCustomer): self
{
$this->idCustomer = $idCustomer;
return $this;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
public function setIdCustomerSpecialty(int $idCustomerSpecialty): self
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
return $this;
}
public function getDateAdd(): ?DateTime
{
return $this->dateAdd;
}
public function setDateAdd(DateTime $dateAdd): self
{
$this->dateAdd = $dateAdd;
return $this;
}
public function getDateUpd(): ?DateTime
{
return $this->dateUpd;
}
public function setDateUpd(DateTime $dateUpd): self
{
$this->dateUpd = $dateUpd;
return $this;
}
public function toArray(): array
{
return [
'id_ws_customerspecialty_customer' => $this->id,
'id_customer' => $this->idCustomer,
'id_ws_customerspecialty' => $this->idCustomerSpecialty,
'date_add' => $this->dateAdd ? $this->dateAdd->format('Y-m-d H:i:s') : null,
'date_upd' => $this->dateUpd ? $this->dateUpd->format('Y-m-d H:i:s') : null,
];
}
}
@@ -0,0 +1,85 @@
<?php
namespace Websmid\CustomerSpecialty\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="ws_customerspecialty_lang")
* @ORM\Entity()
*/
class CustomerSpecialtyLang
{
/**
* @ORM\Id
* @ORM\Column(name="id_ws_customerspecialty_lang", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="id_ws_customerspecialty", type="integer")
*/
private $idCustomerSpecialty;
/**
* @ORM\Column(name="id_lang", type="integer")
*/
private $idLang;
/**
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
public function getId(): ?int
{
return $this->id;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
public function setIdCustomerSpecialty(int $idCustomerSpecialty): self
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
return $this;
}
public function getIdLang(): int
{
return $this->idLang;
}
public function setIdLang(int $idLang): self
{
$this->idLang = $idLang;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function toArray(): array
{
return [
'id_ws_customerspecialty_lang' => $this->id,
'id_ws_customerspecialty' => $this->idCustomerSpecialty,
'id_lang' => $this->idLang,
'title' => $this->title,
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace Websmid\CustomerSpecialty\Entity;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
class TablePrefixSubscriber implements EventSubscriber
{
private $prefix;
public function __construct(string $prefix)
{
$this->prefix = $prefix ?: 'ps_';
}
public function getSubscribedEvents(): array
{
return [Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args): void
{
$classMetadata = $args->getClassMetadata();
if (strpos($classMetadata->getName(), 'Websmid\\CustomerSpecialty\\') !== 0) {
return;
}
$tableName = $classMetadata->getTableName();
if (strpos($tableName, $this->prefix) === 0) {
return;
}
$classMetadata->setPrimaryTable(['name' => $this->prefix . $tableName]);
}
}
@@ -0,0 +1,45 @@
<?php
namespace Websmid\CustomerSpecialty\Form\ChoiceProvider;
use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface;
use PrestaShop\PrestaShop\Core\Form\FormChoiceAttributeProviderInterface;
use PrestaShop\PrestaShop\Core\Form\FormChoiceProviderInterface;
use Context;
use Db;
final class CustomerSpecialtyChoiceProvider implements
FormChoiceProviderInterface,
FormChoiceAttributeProviderInterface,
ConfigurableFormChoiceProviderInterface
{
public function getChoices(array $options = []): array
{
$includeInactive = $options['include_inactive'] ?? false;
$idLang = (int) Context::getContext()->language->id;
$sql = '
SELECT cs.`id_ws_customerspecialty`, csl.`title`
FROM `' . _DB_PREFIX_ . 'ws_customerspecialty` cs
INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl
ON csl.`id_ws_customerspecialty` = cs.`id_ws_customerspecialty`
AND csl.`id_lang` = ' . $idLang . '
' . ($includeInactive ? '' : 'WHERE cs.`active` = 1') . '
ORDER BY cs.`position` ASC
';
$rows = Db::getInstance()->executeS($sql);
$choices = [];
foreach ($rows as $row) {
$choices[$row['title']] = (int) $row['id_ws_customerspecialty'];
}
return $choices;
}
public function getChoicesAttributes(): array
{
return [];
}
}
@@ -0,0 +1,36 @@
<?php
namespace Websmid\CustomerSpecialty\Form\DataHandler;
use PrestaShop\PrestaShop\Core\Form\FormDataHandlerInterface;
use Websmid\CustomerSpecialty\Command\SaveCustomerSpecialtyCommand;
use PrestaShop\PrestaShop\Core\CommandBus\CommandBusInterface;
final class CustomerSpecialtyDataHandler implements FormDataHandlerInterface
{
private $commandBus;
public function __construct(CommandBusInterface $commandBus)
{
$this->commandBus = $commandBus;
}
public function create(array $data)
{
return $this->save(0, $data);
}
public function update($id, array $data)
{
return $this->save((int) $id, $data);
}
private function save(int $id, array $data)
{
return $this->commandBus->handle(new SaveCustomerSpecialtyCommand(
$id,
(bool) ($data['active'] ?? false),
$data['title'] ?? []
));
}
}
@@ -0,0 +1,47 @@
<?php
namespace Websmid\CustomerSpecialty\Form\DataProvider;
use PrestaShop\PrestaShop\Core\Form\FormDataProviderInterface;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialty;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialtyLang;
use Doctrine\ORM\EntityManagerInterface;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
final class CustomerSpecialtyDataProvider implements FormDataProviderInterface
{
public function getDefaultData(): array
{
return [
'title' => [],
'active' => true,
];
}
public function getData($id): array
{
$entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$customerSpecialty = $entityManager->getRepository(CustomerSpecialty::class)->find((int) $id);
if (!$customerSpecialty) {
return $this->getDefaultData();
}
return [
'title' => $this->loadTitles($entityManager, (int) $id),
'active' => $customerSpecialty->isActive(),
];
}
private function loadTitles(EntityManagerInterface $entityManager, int $idCustomerSpecialty): array
{
$titles = [];
foreach ($entityManager->getRepository(CustomerSpecialtyLang::class)->findBy(['idCustomerSpecialty' => $idCustomerSpecialty]) as $lang) {
$titles[$lang->getIdLang()] = $lang->getTitle();
}
return $titles;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Websmid\CustomerSpecialty\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use Symfony\Contracts\Translation\TranslatorInterface;
class CustomerSpecialtyType extends AbstractType
{
private $translator;
private $locales;
public function __construct(TranslatorInterface $translator, array $locales)
{
$this->translator = $translator;
$this->locales = $locales;
}
public function buildForm(FormBuilderInterface $formBuilder, array $options): void
{
$formBuilder
->add('title', TranslatableType::class, [
'type' => TextType::class,
'label' => $this->trans('Title'),
'required' => true,
])
->add('active', SwitchType::class, [
'label' => $this->trans('Active'),
'required' => false,
]);
}
private function trans(string $label): string
{
return $this->translator->trans($label, [], 'Modules.Wscustomerspecialty.Admin');
}
}
@@ -0,0 +1,139 @@
<?php
namespace Websmid\CustomerSpecialty\Grid\Definition\Factory;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\AbstractGridDefinitionFactory;
use PrestaShop\PrestaShop\Core\Grid\Column\ColumnCollection;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\ToggleColumn;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\ActionColumn;
use PrestaShop\PrestaShop\Core\Grid\Action\Row\RowActionCollection;
use PrestaShop\PrestaShop\Core\Grid\Action\Row\Type\LinkRowAction;
use PrestaShop\PrestaShop\Core\Grid\Action\GridActionCollection;
use PrestaShop\PrestaShop\Core\Grid\Action\Type\SimpleGridAction;
use PrestaShop\PrestaShop\Core\Grid\Filter\Filter;
use PrestaShop\PrestaShop\Core\Grid\Filter\FilterCollection;
use PrestaShopBundle\Form\Admin\Type\SearchAndResetType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class CustomerSpecialtyGridDefinitionFactory extends AbstractGridDefinitionFactory
{
const GRID_ID = 'customerspecialty';
protected function getId()
{
return self::GRID_ID;
}
protected function getName()
{
return $this->trans('Customer Specialties', [], 'Modules.Wscustomerspecialty.Admin');
}
protected function getColumns()
{
return (new ColumnCollection())
->add(
(new DataColumn('id_ws_customerspecialty'))
->setName($this->trans('ID', [], 'Admin.Global'))
->setOptions([
'field' => 'id_ws_customerspecialty',
])
)
->add(
(new DataColumn('title'))
->setName($this->trans('Title', [], 'Modules.Wscustomerspecialty.Admin'))
->setOptions([
'field' => 'title',
])
)
->add(
(new ToggleColumn('active'))
->setName($this->trans('Active', [], 'Admin.Global'))
->setOptions([
'field' => 'active',
'primary_field' => 'id_ws_customerspecialty',
'route' => 'customerspecialty_toggle_active',
'route_param_name' => 'idCustomerSpecialty',
])
)
->add(
(new ActionColumn('actions'))
->setName($this->trans('Actions', [], 'Admin.Global'))
->setOptions([
'actions' => (new RowActionCollection())
->add(
(new LinkRowAction('edit'))
->setName($this->trans('Edit', [], 'Admin.Actions'))
->setIcon('edit')
->setOptions([
'route' => 'customerspecialty_edit',
'route_param_name' => 'idCustomerSpecialty',
])
)
->add(
(new LinkRowAction('move_up'))
->setName($this->trans('Move up', [], 'Modules.Wscustomerspecialty.Admin'))
->setIcon('arrow_upward')
->setOptions([
'route' => 'customerspecialty_move_up',
'route_param_name' => 'idCustomerSpecialty',
])
)
->add(
(new LinkRowAction('move_down'))
->setName($this->trans('Move down', [], 'Modules.Wscustomerspecialty.Admin'))
->setIcon('arrow_downward')
->setOptions([
'route' => 'customerspecialty_move_down',
'route_param_name' => 'idCustomerSpecialty',
])
),
])
);
}
protected function getFilters()
{
return (new FilterCollection())
->add(
(new Filter('title', TextType::class))
->setAssociatedColumn('title')
)
->add(
(new Filter('active', ChoiceType::class))
->setTypeOptions([
'choices' => [
$this->trans('Active', [], 'Admin.Global') => 1,
$this->trans('Inactive', [], 'Admin.Global') => 0,
],
'required' => false,
])
->setAssociatedColumn('active')
)
->add(
(new Filter('actions', SearchAndResetType::class))
->setTypeOptions([
'reset_route' => 'customerspecialty_index',
'redirect_route' => 'customerspecialty_index',
])
->setAssociatedColumn('actions')
);
}
protected function getGridActions()
{
return (new GridActionCollection())
->add(
(new SimpleGridAction('common_refresh_list'))
->setName($this->trans('Refresh list', [], 'Admin.Catalog.Feature'))
->setIcon('refresh')
)
->add(
(new SimpleGridAction('common_show_query'))
->setName($this->trans('Show SQL query', [], 'Admin.Catalog.Feature'))
->setIcon('code')
);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Websmid\CustomerSpecialty\Grid\Filters;
use PrestaShop\PrestaShop\Core\Grid\Filter\Filters;
use Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory;
class CustomerSpecialtyFilters extends Filters
{
protected $filterId = CustomerSpecialtyGridDefinitionFactory::GRID_ID;
public static function getDefaults()
{
return [
'limit' => 50,
'offset' => 0,
'orderBy' => 'cs.position',
'sortOrder' => 'asc',
'filters' => [],
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace Websmid\CustomerSpecialty\Grid\Query;
use PrestaShop\PrestaShop\Core\Grid\Query\AbstractDoctrineQueryBuilder;
use PrestaShop\PrestaShop\Core\Grid\Search\SearchCriteriaInterface;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
class CustomerSpecialtyQueryBuilder extends AbstractDoctrineQueryBuilder
{
private $searchCriteriaApplicator;
private $contextLanguageId;
private $filterApplicator;
public function __construct(
Connection $connection,
$dbPrefix,
$searchCriteriaApplicator,
int $contextLanguageId,
$filterApplicator
) {
parent::__construct($connection, $dbPrefix);
$this->searchCriteriaApplicator = $searchCriteriaApplicator;
$this->contextLanguageId = $contextLanguageId;
$this->filterApplicator = $filterApplicator;
}
public function getSearchQueryBuilder(SearchCriteriaInterface $searchCriteria)
{
$queryBuilder = $this->getBaseQuery();
$queryBuilder->select('cs.id_ws_customerspecialty, cs.active, cs.position, csl.title');
$this->applyFilters($queryBuilder, $searchCriteria);
$this->searchCriteriaApplicator
->applyPagination($searchCriteria, $queryBuilder)
->applySorting($searchCriteria, $queryBuilder);
return $queryBuilder;
}
public function getCountQueryBuilder(SearchCriteriaInterface $searchCriteria)
{
$queryBuilder = $this->getBaseQuery();
$queryBuilder->select('COUNT(cs.id_ws_customerspecialty)');
$this->applyFilters($queryBuilder, $searchCriteria);
return $queryBuilder;
}
private function getBaseQuery(): QueryBuilder
{
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->from($this->dbPrefix . 'ws_customerspecialty', 'cs')
->leftJoin(
'cs',
$this->dbPrefix . 'ws_customerspecialty_lang',
'csl',
'csl.id_ws_customerspecialty = cs.id_ws_customerspecialty AND csl.id_lang = :contextLanguageId'
)
->setParameter('contextLanguageId', $this->contextLanguageId);
return $queryBuilder;
}
private function applyFilters(QueryBuilder $queryBuilder, SearchCriteriaInterface $searchCriteria): void
{
foreach ($searchCriteria->getFilters() as $filterName => $filterValue) {
if ('' === $filterValue || null === $filterValue) {
continue;
}
switch ($filterName) {
case 'title':
$queryBuilder->andWhere('csl.title LIKE :title')
->setParameter('title', '%' . pSQL($filterValue) . '%');
break;
case 'active':
$queryBuilder->andWhere('cs.active = :active')
->setParameter('active', (int) $filterValue);
break;
}
}
}
}
@@ -0,0 +1,206 @@
<?php
namespace Websmid\CustomerSpecialty\Module;
use Websmid\CustomerSpecialty\Config\Config;
use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand;
use Db;
use Configuration;
use Tools;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
use PrestaShop\PrestaShop\Core\Grid\Filter\Filter;
trait AdminHooks
{
public function isUsingNewTranslationSystem(): bool
{
return true;
}
public function getContent()
{
$confirmation = '';
if (Tools::isSubmit('submitWsCustomerSpecialty')) {
Configuration::updateValue(Config::FRONT_ENABLED_CONFIG_KEY, (bool) Tools::getValue('front_enabled'));
$confirmation = $this->displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success'));
}
$this->context->smarty->assign([
'front_enabled' => (bool) Configuration::get(Config::FRONT_ENABLED_CONFIG_KEY),
'form_action' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name,
'confirmation' => $confirmation,
]);
return $this->fetch('module:ws_customerspecialty/views/templates/admin/configuration.tpl');
}
public function hookActionCustomerFormBuilderModifier(array $params): void
{
/** @var FormBuilderInterface $formBuilder */
$formBuilder = $params['form_builder'];
$idCustomer = (int) $params['id'];
$formBuilder->add('specialization', ChoiceType::class, [
'label' => $this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Admin'),
'required' => true,
'placeholder' => $this->trans('Please select', [], 'Admin.Global'),
'choices' => $this->getCustomerSpecialtyChoiceProvider()->getChoices([
'include_inactive' => $idCustomer > 0,
]),
]);
if ($idCustomer > 0) {
$params['data']['specialization'] = $this->loadCustomerSpecialtyId($idCustomer);
$formBuilder->setData($params['data']);
}
}
public function hookActionAfterCreateCustomerFormHandler(array $params): void
{
$this->persistBackOfficeCustomerSpecialty($params);
}
public function hookActionAfterUpdateCustomerFormHandler(array $params): void
{
$this->persistBackOfficeCustomerSpecialty($params);
}
public function hookActionCustomerGridDefinitionModifier(array $params): void
{
$definition = $params['definition'];
$definition->getColumns()->addBefore(
'social_title',
(new DataColumn('specialization'))
->setName($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Admin'))
->setOptions([
'field' => 'specialization',
])
);
$choices = ['--' => 0] + $this->getCustomerSpecialtyChoiceProvider()->getChoices(['include_inactive' => true]);
$definition->getFilters()->add(
(new Filter('specialization', ChoiceType::class))
->setTypeOptions([
'choices' => $choices,
'required' => false,
])
->setAssociatedColumn('specialization')
);
}
public function hookActionCustomerGridQueryBuilderModifier(array $params): void
{
$searchQueryBuilder = $params['search_query_builder'];
$searchCriteria = $params['search_criteria'];
$idLang = (int) $this->context->language->id;
$searchQueryBuilder->addSelect(
'(SELECT csl.title
FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu
INNER JOIN ' . _DB_PREFIX_ . 'ws_customerspecialty_lang csl
ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . '
WHERE cscu.id_customer = c.id_customer
) as specialization'
);
$this->applyCustomerSpecialtySorting($searchCriteria, $searchQueryBuilder);
$this->applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder);
}
public function hookDisplayAdminCustomers(array $params)
{
$title = $this->loadCustomerSpecialtyTitle((int) $params['id_customer']);
if (null === $title) {
return '';
}
$this->context->smarty->assign(['specialization' => $title]);
return $this->fetch('module:ws_customerspecialty/views/templates/hook/display_admin_customers.tpl');
}
private function persistBackOfficeCustomerSpecialty(array $params): void
{
$idCustomer = (int) $params['id'];
$specialization = (int) ($params['form_data']['specialization'] ?? 0);
if ($idCustomer <= 0 || $specialization <= 0) {
return;
}
$this->get('prestashop.core.command_bus')->handle(
new LinkCustomerSpecialtyCommand($idCustomer, $specialization)
);
}
private function applyCustomerSpecialtySorting($searchCriteria, $searchQueryBuilder): void
{
if ('specialization' !== $searchCriteria->getOrderBy()) {
return;
}
$searchQueryBuilder->orderBy('specialization', $searchCriteria->getOrderWay());
}
private function applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder): void
{
$filters = $searchCriteria->getFilters();
if (!isset($filters['specialization']) || '' === $filters['specialization']) {
return;
}
$idCustomerSpecialty = (int) $filters['specialization'];
if (0 === $idCustomerSpecialty) {
$searchQueryBuilder->andWhere(
'NOT EXISTS (SELECT 1 FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu WHERE cscu.id_customer = c.id_customer)'
);
return;
}
$searchQueryBuilder
->andWhere(
'EXISTS (
SELECT 1 FROM ' . _DB_PREFIX_ . 'ws_customerspecialty_customer cscu
WHERE cscu.id_customer = c.id_customer AND cscu.id_ws_customerspecialty = :idCustomerSpecialtyFilter
)'
)
->setParameter('idCustomerSpecialtyFilter', $idCustomerSpecialty);
}
private function loadCustomerSpecialtyId(int $idCustomer): int
{
return (int) Db::getInstance()->getValue(
'SELECT `id_ws_customerspecialty` FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer`
WHERE `id_customer` = ' . $idCustomer
);
}
private function loadCustomerSpecialtyTitle(int $idCustomer): ?string
{
$idLang = (int) $this->context->language->id;
$title = Db::getInstance()->getValue('
SELECT csl.title
FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` cscu
INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl
ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . '
WHERE cscu.id_customer = ' . $idCustomer . '
');
return false !== $title && null !== $title ? (string) $title : null;
}
private function getCustomerSpecialtyChoiceProvider()
{
return $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider');
}
}
@@ -0,0 +1,121 @@
<?php
namespace Websmid\CustomerSpecialty\Module;
use Websmid\CustomerSpecialty\Config\Config;
use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand;
use Configuration;
use Tools;
use Db;
use PrestaShop\PrestaShop\Core\Form\FormField;
trait DisplayHooks
{
public function hookAdditionalCustomerFormFields(array $params)
{
if (!$this->isFrontEnabled()) {
return;
}
$choices = $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider')->getChoices();
if (empty($choices)) {
return;
}
$specializationField = (new FormField())
->setName('specialization')
->setType('select')
->setLabel($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Shop'))
->setRequired(true)
->setAvailableValues($choices);
if (isset($params['fields'])) {
$this->insertFieldAfterEmail($params['fields'], $specializationField);
return;
}
return $specializationField;
}
public function hookActionObjectCustomerAddAfter(array $params): void
{
$this->persistFrontOfficeCustomerSpecialty($params);
}
public function hookActionObjectCustomerUpdateAfter(array $params): void
{
$this->persistFrontOfficeCustomerSpecialty($params);
}
public function hookActionGetExtraMailTemplateVars(array $params): void
{
$specialization = $this->loadOrderCustomerSpecialization((int) $params['template_vars']['{id_order}']);
if (null === $specialization) {
return;
}
$params['extra_template_vars']['{specialization}'] = $specialization;
}
private function insertFieldAfterEmail(array &$fields, FormField $field): void
{
$keys = array_keys($fields);
$emailPosition = array_search('email', $keys, true);
$insertAt = false === $emailPosition ? count($fields) : $emailPosition + 1;
$before = array_slice($fields, 0, $insertAt, true);
$after = array_slice($fields, $insertAt, null, true);
$fields = $before + ['specialization' => $field] + $after;
}
private function persistFrontOfficeCustomerSpecialty(array $params): void
{
if (!$this->isFrontEnabled()) {
return;
}
$customer = $params['object'];
$idCustomer = (int) $customer->id;
$specialization = (int) Tools::getValue('specialization');
if ($idCustomer <= 0 || $specialization <= 0) {
return;
}
$this->get('prestashop.core.command_bus')->handle(
new LinkCustomerSpecialtyCommand($idCustomer, $specialization)
);
}
private function loadOrderCustomerSpecialization(int $idOrder): ?string
{
$idCustomer = (int) Db::getInstance()->getValue(
'SELECT `id_customer` FROM `' . _DB_PREFIX_ . 'orders` WHERE `id_order` = ' . $idOrder
);
if (!$idCustomer) {
return null;
}
$idLang = (int) $this->context->language->id;
$title = Db::getInstance()->getValue('
SELECT csl.title
FROM `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` cscu
INNER JOIN `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` csl
ON csl.id_ws_customerspecialty = cscu.id_ws_customerspecialty AND csl.id_lang = ' . $idLang . '
WHERE cscu.id_customer = ' . $idCustomer . '
');
return false !== $title && null !== $title ? (string) $title : null;
}
private function isFrontEnabled(): bool
{
return (bool) Configuration::get(Config::FRONT_ENABLED_CONFIG_KEY);
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace Websmid\CustomerSpecialty\Module;
use Websmid\CustomerSpecialty\Config\Config;
use Db;
use Language;
use Tab;
use Configuration;
trait Install
{
public function install()
{
if (!parent::install()) {
return false;
}
if (!$this->createTables()) {
return false;
}
if (!$this->registerTabs(Config::TABS)) {
return false;
}
if (!$this->registerHooks()) {
return false;
}
Configuration::updateValue(Config::FRONT_ENABLED_CONFIG_KEY, false);
return true;
}
private function createTables(): bool
{
$sqlToExec = [
'
CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty` (
`id_ws_customerspecialty` INT(11) NOT NULL AUTO_INCREMENT,
`position` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_ws_customerspecialty`)
);
',
'
CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty_lang` (
`id_ws_customerspecialty_lang` INT(11) NOT NULL AUTO_INCREMENT,
`id_ws_customerspecialty` INT(11) NOT NULL,
`id_lang` INT(11) NOT NULL,
`title` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id_ws_customerspecialty_lang`),
UNIQUE KEY `ws_customerspecialty_lang_unique` (`id_ws_customerspecialty`, `id_lang`)
);
',
'
CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customerspecialty_customer` (
`id_ws_customerspecialty_customer` INT(11) NOT NULL AUTO_INCREMENT,
`id_customer` INT(11) NOT NULL,
`id_ws_customerspecialty` INT(11) NOT NULL,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_ws_customerspecialty_customer`),
UNIQUE KEY `ws_customerspecialty_customer_unique` (`id_customer`),
KEY `id_ws_customerspecialty` (`id_ws_customerspecialty`)
);
',
];
foreach ($sqlToExec as $sql) {
if (!Db::getInstance()->execute($sql)) {
return false;
}
}
return true;
}
private function registerHooks(): bool
{
foreach (Config::HOOKS as $hook) {
if (!$this->registerHook($hook)) {
return false;
}
}
return true;
}
private function registerTabs(array $moduleTabs): bool
{
$languages = Language::getLanguages(false);
foreach ($moduleTabs as $moduleTab) {
if (Tab::getIdFromClassName($moduleTab['class_name'])) {
continue;
}
$tab = new Tab();
$tab->class_name = $moduleTab['class_name'];
$tab->module = $moduleTab['module'];
$tab->active = (int) $moduleTab['active'];
$tab->route_name = $moduleTab['route_name'];
$tab->icon = $moduleTab['icon'];
$tab->id_parent = (int) Tab::getIdFromClassName($moduleTab['parent_class_name']);
foreach ($languages as $language) {
$tab->name[(int) $language['id_lang']] = $moduleTab['name'][$language['iso_code']]
?? $moduleTab['name']['en'];
}
$tab->add();
if (!$tab->id) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,29 @@
{if $confirmation}
{$confirmation nofilter}
{/if}
<div class="panel">
<div class="panel-heading">
<i class="icon-user"></i>
{l s='Front office display' d='Modules.Wscustomerspecialty.Admin'}
</div>
<div class="panel-body">
<p class="help-block">
{l s='When enabled, a required "Specialty" field is added to the registration and account edit forms on the storefront. Back-office management of specialty values and customer records is unaffected by this setting.' d='Modules.Wscustomerspecialty.Admin'}
</p>
<form method="post" action="{$form_action|escape:'htmlall':'UTF-8'}">
<div class="form-group">
<label class="control-label">
<input type="checkbox" name="front_enabled" value="1" {if $front_enabled}checked="checked"{/if}>
{l s='Show the specialty field on the storefront' d='Modules.Wscustomerspecialty.Admin'}
</label>
</div>
<div class="panel-footer">
<button type="submit" name="submitWsCustomerSpecialty" class="btn btn-default pull-right">
<i class="process-icon-save"></i>
{l s='Save' d='Modules.Wscustomerspecialty.Admin'}
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,23 @@
{% import '@PrestaShop/Admin/macros.html.twig' as ps %}
{% form_theme customerSpecialtyForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %}
{% block customerspecialty_form_block %}
<div class="card">
{{ form_start(customerSpecialtyForm) }}
<div class="card-header">
<h3 class="card-header-title">{{ 'Customer specialty'|trans({}, 'Modules.Wscustomerspecialty.Admin') }}</h3>
</div>
<div class="card-body">
{{ form_rest(customerSpecialtyForm) }}
</div>
<div class="card-footer">
<a href="{{ path('customerspecialty_index') }}" class="btn btn-outline-secondary">
{{ 'Cancel'|trans({}, 'Admin.Actions') }}
</a>
<button class="btn btn-primary float-right">
{{ 'Save'|trans({}, 'Admin.Actions') }}
</button>
</div>
{{ form_end(customerSpecialtyForm) }}
</div>
{% endblock %}
@@ -0,0 +1,19 @@
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
{% block customerspecialty_form %}
{% include '@Modules/ws_customerspecialty/views/templates/admin/form/blocks/form.html.twig' %}
{% endblock %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
$(document).ready(function () {
window.prestashop.component.initComponents([
'TranslatableField',
'TranslatableInput',
]);
});
</script>
{% endblock %}
@@ -0,0 +1,14 @@
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
{% include '@PrestaShop/Admin/Common/Grid/grid_panel.html.twig' with {'grid': customerSpecialtyGrid} %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
$(document).ready(function () {
new window.prestashop.component.Grid('customerspecialty');
});
</script>
{% endblock %}
@@ -0,0 +1,11 @@
{if $specialization}
<div class="panel" id="ws-customerspecialty-panel">
<div class="panel-heading">
<i class="icon-user-md"></i>
{l s='Specialty' d='Modules.Wscustomerspecialty.Admin'}
</div>
<div class="panel-body">
<p class="ws-customerspecialty-title">{$specialization|escape:'htmlall':'UTF-8'}</p>
</div>
</div>
{/if}
@@ -0,0 +1,55 @@
<?php
/**
* 2007-2026 De Websmid BV
*
* NOTICE OF LICENSE
*
* This file is not open source! Each license that you purchased is only available for 1 website only.
* If you want to use this file on more websites (or projects), you need to purchase additional licenses.
* You are not allowed to redistribute, resell, lease, license, sub-license or offer our resources to any third party.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please contact us for extra customization service at an affordable price
*
* @author Isabelle Oving-Anno | De Websmid <isabelle@dewebsmid.nl>
* @copyright 2007-2026 De Websmid BV
* @license Valid for 1 website (or project) for each purchase of license
* International Registered Trademark & Property of De Websmid BV
*/
use Websmid\CustomerSpecialty\Module\Install;
use Websmid\CustomerSpecialty\Module\AdminHooks;
use Websmid\CustomerSpecialty\Module\DisplayHooks;
if (!defined('_PS_VERSION_')) {
exit;
}
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
class Ws_CustomerSpecialty extends Module
{
use Install;
use AdminHooks;
use DisplayHooks;
public function __construct()
{
$this->name = 'ws_customerspecialty';
$this->version = '1.0.0';
$this->author = 'Isabelle Oving-Anno | De Websmid BV';
$this->need_instance = 0;
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->trans('Customer Specialty', [], 'Modules.Wscustomerspecialty.Admin');
$this->description = $this->trans('Adds a manageable specialty/profession field to customer accounts, shown in the back office and exposed to order confirmation emails.', [], 'Modules.Wscustomerspecialty.Admin');
$this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_];
}
}