Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 188 additions & 42 deletions modules/concepts/controllers/admin-controllers/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,179 @@ Using modern pages, you will have access to the PrestaShop debug toolbar, the se

## How to declare a new Controller

Somewhere in your module declare a new class that will act as a Controller:
{{% notice warning %}}
**PrestaShop 9.0+**: The `FrameworkBundleAdminController` is **deprecated** and will be removed in PrestaShop 10.0. Use `PrestaShopAdminController` instead for new controllers.
{{% /notice %}}

In your module, create a new controller class in the `src/Controller/` directory:

```php
<?php
// modules/your-module/src/Controller/DemoController.php

namespace MyModule\Controller;

use Doctrine\Common\Cache\CacheProvider;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Controller\Admin\PrestaShopAdminController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DemoController extends FrameworkBundleAdminController
class DemoController extends PrestaShopAdminController
{
private $cache;

// you can use symfony DI to inject services
public function __construct(CacheProvider $cache)
/**
* Inject services via constructor for services used across multiple actions
*/
public function __construct(
private readonly MyCustomService $myCustomService,
) {
}

/**
* You can also inject services directly in action methods
*/
public function demoAction(AnotherService $anotherService): Response
{
$this->cache = $cache;
// Use injected services
$data = $this->myCustomService->getData();
$moreData = $anotherService->getMoreData();

return $this->render('@Modules/your-module/templates/admin/demo.html.twig', [
'data' => $data,
'moreData' => $moreData,
]);
}

public function demoAction()
/**
* You can also use the #[Autowire] attribute to inject services by their ID
*/
public function advancedAction(
Request $request,
ShopRepository $shopRepository,
#[Autowire(service: 'my_module.custom_service')] $customService,
): Response
{
return $this->render('@Modules/your-module/templates/admin/demo.html.twig');
// Use the injected service
$customService->execute();

return $this->render('@Modules/your-module/templates/admin/advanced.html.twig');
}

/**
* Use helper methods provided by PrestaShopAdminController
*/
public function anotherAction(): Response
{
// PrestaShopAdminController provides helper methods for common services
$config = $this->getConfiguration()->get('my_config');

return $this->render('@Modules/your-module/templates/admin/another.html.twig',[
'my_config' => $config,
]);
}
}
```

The example above demonstrates a complete controller implementation with:
- **Constructor injection** for services used across multiple actions (`MyCustomService`)
- **Method parameter injection** for services used in specific actions (`AnotherService`)
- **Autowire attribute** for injecting services by their ID (`#[Autowire(service: 'my_module.custom_service')]`)
- **Helper methods** provided by `PrestaShopAdminController` for accessing common PrestaShop services

You have access to Twig as rendering engine and everything from the Symfony framework ecosystem.
Note that you must return a `Response` object, but this can be a `JsonResponse` if you plan to make a single page application (or "SPA").

{{% notice note %}}
This controller works exactly the same as the Core Back Office ones.
{{% /notice %}}

{{% notice info %}}
**Accessing PrestaShop services:**

The `PrestaShopAdminController` provides helper methods to access common PrestaShop services:
- `$this->getConfiguration()` - Configuration service
- `$this->getTranslator()` or `$this->trans()` - Translation service
- `$this->getRouter()` - Router service
- `$this->getEmployeeContext()` - Employee context
- `$this->getShopContext()` - Shop context
- `$this->getLanguageContext()` - Language context
- `$this->getCurrencyContext()` - Currency context
- `$this->getCountryContext()` - Country context
- `$this->dispatchCommand()` - Execute CQRS commands
- `$this->dispatchQuery()` - Execute CQRS queries
- `$this->dispatchHookWithParameters()` - Dispatch hooks
- `$this->presentGrid()` - Present grid data

See the complete list in the [PrestaShopAdminController source code](https://github.com/PrestaShop/PrestaShop/blob/9.0.0/src/PrestaShopBundle/Controller/Admin/PrestaShopAdminController.php).
{{% /notice %}}

{{% notice warning %}}
**Doctrine repositories must be injected:**

Doctrine ORM is not directly accessible via `$this->getDoctrine()` or similar methods. All Doctrine repositories and entity managers must be injected as dependencies in your controller's constructor or action methods.

Example:
```php
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\ProductRepository;

public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ProductRepository $productRepository,
) {
}
```
{{% /notice %}}

## Service Configuration

In PrestaShop 9.0, controllers must be defined as services. You have two main approaches to configure your controller service:

### Option 1: Explicit service configuration with tags

If you want Symfony Dependency Injection to inject services into your controller, you need to use specific YAML service declaration:
```yaml
# modules/your-module/config/services.yml
services:
# The name of the service must match the full namespace class
MyModule\Controller\DemoController:
class: MyModule\Controller\DemoController
arguments:
- '@doctrine.cache.provider'
autowire: true
autoconfigure: true
tags:
- { name: controller.service_arguments }
Comment on lines +146 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these ones are doublons, you can either use the tag alon Or the autoconfigure alone but both are doing almost the same thing

So I would favor relying on autoconfigure: true and removing the tag part to keep the definition as simple as possible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, regarding the autowiring, maybe we should add an example when you want to inject a service manually But we're entering more in Symfony many possibilities for DI, so instead of redocumenting Symfony we should probably add a notice about the pre-requisite knowledge from the framework itself

The same links that were added in the 9.0 changes pages:

and maybe https://symfony.com/doc/6.4/controller.html for a more generic view on what a Symfony controller can do

```

You can also retrieve services with the container available in symfony controllers ->
```php
<?php
// modules/your-module/src/Controller/DemoController.php
### Option 2: Using service defaults

namespace MyModule\Controller;
```yaml
# modules/your-module/config/services.yml
services:
_defaults:
public: false
autowire: true
autoconfigure: true

MyModule\Controller\DemoController: ~
```

use Doctrine\Common\Cache\CacheProvider;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
{{% notice warning %}}
**Service configuration is mandatory:**

class DemoController extends FrameworkBundleAdminController
{
public function demoAction()
{
// you can also retrieve services directly from the container
$cache = $this->container->get('doctrine.cache');

return $this->render('@Modules/your-module/templates/admin/demo.html.twig');
}
}
```
One of the two service configuration options above is **essential and required** for your controller to work properly. Without this configuration:
- **The controller will not function** and will throw errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **The controller will not function** and will throw errors
- **The controller will not work** and will throw errors

I'm not sure which one is more correct in english, maybe I'm biased with my french point of view 😅

- Helper methods from `PrestaShopAdminController` will not be accessible
- Services like Twig, Form, Router, and other Symfony components will not be available

You have access to the Container, to Twig as rendering engine, the Doctrine ORM, everything from Symfony framework ecosystem.
Note that you must return a `Response` object, but this can be a `JsonResponse` if you plan to make a single page application (or "SPA").
**Important:** The service name (e.g., `MyModule\Controller\DemoController`) **must exactly match** the fully qualified class name (FQCN) of your controller. If the service name doesn't match the class name, Symfony and PrestaShop will not be able to identify and instantiate the controller, resulting in errors.

{{% notice note %}}
This controller works exactly the same as the Core Back Office ones.
{{% /notice %}}
**Understanding the configuration:**
- `autowire: true` - Automatically injects services in constructors and method parameters
- `autoconfigure: true` - Automatically configures the controller as a service and enables all controller features
- `controller.service_arguments` tag - Required if your controller doesn't extend `AbstractController` to enable method parameter injection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true! But should we mention this? And by that I mean should we encourage people having controller that do not extend AbstractController, so not PrestaShopAdminController in the end

Or should we simply document: Use this class PrestaShopAdminController and that's enough documentation because any other type of controllers means you know what you're doing with the Symfony framework, so you are on your own because it's your specific need and your expertise

{{% /notice %}}

## Autoloading Configuration

You must enable the autoloading for this Controller. For example using a `composer.json` file for your module.

#### Example using PSR-4 namespacing
### Example using PSR-4 namespacing

1. Use namespace for your Controller file

Expand All @@ -87,7 +191,7 @@ You must enable the autoloading for this Controller. For example using a `compos

namespace MyModule\Controller;

use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Controller\Admin\PrestaShopAdminController;
```

2. Configure composer to autoload this namespace
Expand All @@ -98,7 +202,7 @@ You must enable the autoloading for this Controller. For example using a `compos
"description": "...",
"autoload": {
"psr-4": {
"MyModule\\Controller\\": "src/Controller/"
"MyModule\\": "src/"
}
},
"config": {
Expand Down Expand Up @@ -182,6 +286,48 @@ In order to generate the valid URI of a controller you created from inside the m
```


## Key Changes in PrestaShop 9.0 (Symfony 6.4)

### Controllers as Services

In Symfony 6.4, controllers must be defined as services. The container passed to controllers is no longer the "global container" but a dedicated, optimized container based on the services injected into it.

**Important implications:**

- The `$this->get('service_name')` method is no longer available in modern controllers
- `FrameworkBundleAdminController` maintains backward compatibility but is **deprecated** and will be removed in PrestaShop 10.0
- New controllers should extend `PrestaShopAdminController` which follows Symfony best practices

### Dependency Injection Methods

You have three main ways to inject services:

1. **Constructor injection** - For services used across multiple actions
2. **Method injection** - For services used in a single action (more optimized)
3. **Autowire attribute** - Use `#[Autowire(service: 'service_id')]` to inject services by their ID

### PrestaShopAdminController Helper Methods

The new base controller provides convenient helper methods for commonly used services:

- `$this->getConfiguration()` - Access configuration service
- `$this->getTranslator()` - Access translator service
- `$this->getRouter()` - Access router service
- `$this->getFlashBag()` - Access flash messages
Comment on lines +313 to +316
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a doublon with the paragraph above that lists even more thelper methods?


See the [full list of helper methods](https://github.com/PrestaShop/PrestaShop/blob/9.0.0/src/PrestaShopBundle/Controller/Admin/PrestaShopAdminController.php) in the class.

{{% notice info %}}
**Learn more about Symfony controllers:**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's what I mentioned in another comment, perfect!

- [Controllers as services](https://symfony.com/doc/6.4/controller/service.html)
- [Service Subscribers & Locators](https://symfony.com/doc/6.4/service_container/service_subscribers_locators.html)
- [Service container](https://symfony.com/doc/6.4/service_container.html)
{{% /notice %}}

## Secure your controller

It is safer to define permissions required to use your controller, this can be configured using the `#[AdminSecurity]` attribute and some configuration in your routing file. You can read this documentation if you need more details about [Controller Security]({{< ref "/9/development/architecture/migration-guide/controller-routing.md#security" >}}).

{{% notice note %}}
**PrestaShop 9.0+**: The `@AdminSecurity` annotation has been replaced with the `#[AdminSecurity]` attribute following PHP 8 standards.
{{% /notice %}}