Ein Leitfaden für moderne Best Practices in der Entwicklung mit Contao
class ModuleNewsReader extends ModuleNews
{
public function generate()
{
if (TL_MODE == 'BE')
{
$objTemplate = new BackendTemplate('be_wildcard');
$objTemplate->wildcard = '### NEWS READER ###';
$objTemplate->title = $this->headline;
$objTemplate->id = $this->id;
$objTemplate->link = $this->name;
$objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id;
return $objTemplate->parse();
}
// Return if no news item has been specified
if (!$this->Input->get('items'))
{
return '';
}
$this->news_archives = $this->sortOutProtected(deserialize($this->news_archives));
// Return if there are no archives
if (!is_array($this->news_archives) || count($this->news_archives) < 1)
{
return '';
}
return parent::generate();
}
Der erste commit
(David Greminger)
(Christian Schiffler)
Stylepicker4ward
dlh_googlemaps
Manual installation: copy all files to system/modules/
Stylepicker4ward
dlh_googlemaps
Teil der Superglobals - interne Variablen, die überall verfügbar sind
Assoziatives Array, das Referenzen auf alle Variablen enthält
Ein PHP-Objekt, welches für die Instanziierung von Objekten bzw. Services zuständig ist.
Es weiß über seine Abhängigkeiten Bescheid, da alles (vor-)konfiguriert
https://symfony.com/doc/current/components/dependency_injection.html
class Database
{
protected function __construct()
{
$this->resConnection = System::getContainer()->get('database_connection');
if (!\is_object($this->resConnection)) {
throw new \Exception(\sprintf('Could not connect to database (%s)', $this->error));
}
}
public static function getInstance()
{
return static::$objInstance ??= new static();
}
$objContent = Database::getInstance()
->prepare('SELECT type,pid FROM tl_content WHERE id=?')
->execute($id)
;
$id = $objContent->pid;
$cond = $objContent->type;
class StylePickerController extends AbstractBackendController
{
public function __construct(
private readonly Connection $connection // Datenbank Service
) {}
// ...
$objContent = $this->connection->fetchAssociative(
'SELECT type, pid FROM tl_content WHERE id = ?', [$id]
);
$id = $objContent['pid'] ?? null;
$condition = $objContent['type'] ?? null;
und wie das Contao CMS es für Entwickler vereinfacht
Services müssen in der services.yaml definiert werden
services:
_defaults:
autoconfigure: true
ContaoGraveyard\GoogleMapsBundle\Controller\ContentElement\GoogleMapsController:
tags:
-
type: dlh_googlemaps
name: contao.content_element
category: media
template: ce_dlh_googlemaps_default
(früher auch über Annotations)
#[AsContentElement(
type: 'dlh_googlemaps',
category: 'media',
template: 'ce_dlh_googlemaps_default',
)]
class GoogleMapsController extends AbstractContentElementController
{
Und nun?
services:
_defaults:
autoconfigure: true
#ContaoGraveyard\GoogleMapsBundle\Controller\ContentElement\GoogleMapsController: ~
...ContentElement\GoogleMapsController: ~
# Yes, it's invalid, I know
Klar, kannst du machen!
services:
_defaults:
autoconfigure: true
autowire: true
ContaoGraveyard\GoogleMapsBundle\:
resource: '../src/'
exclude: '../src/{Event,Model,Whatever.php}'
Mr. Codestyle wants to talk to you
Public Bundles kennen die Services in deinem Container nicht (in der Theorie)
In der App hat man Kontrolle über alles
Warum?
DO NOT CHANGE THIS ORDER!
/**
* Initialize the controller.
*
* 1. Import user
* 2. Call parent constructor
* 3. Authenticate user
* 4. Load language files
* DO NOT CHANGE THIS ORDER!
*/
public function __construct()
{
$this->import(BackendUser::class, 'User');
$this->import('Database');
$this->User->authenticate();
$this->loadLanguageFile('default');
$this->loadLanguageFile('modules');
}
onLoadCallback!
$GLOBALS['TL_DCA']['tl_dlh_googlemaps'] = [
'config' => [
// ...
'onload_callback' => [
['tl_dlh_googlemaps', 'checkPermission'],
],
],
//...
public function checkPermission(): void
{
if ($this->User->isAdmin) {
return;
}
// Set root IDs
if (!is_array($this->User->dlh_googlemapss) || $this->User->dlh_googlemapss === []) {
$root = [0];
} else {
$root = $this->User->dlh_googlemapss;
}
$GLOBALS['TL_DCA']['tl_dlh_googlemaps']['list']['sorting']['root'] = $root;
// Check permissions to add Maps
if (!$this->User->hasAccess('create', 'dlh_googlemapsp')) {
$GLOBALS['TL_DCA']['tl_dlh_googlemaps']['config']['closed'] = \true;
}
// Check current action
switch (Input::get('act')) {
case 'create':
case 'select':
// Allow
break;
case 'edit':
// Dynamically add the record to the user profile
if (!in_array(Input::get('id'), $root, true)) {
$arrNew = $this->Session->get('new_records');
if (is_array($arrNew['tl_dlh_googlemaps']) && in_array(Input::get('id'), $arrNew['tl_dlh_googlemaps'], true)) {
// Viel Code um Berechtigungen auf Gruppenepene upzudaten
// ...
}
}
// no break;
case 'copy':
case 'delete':
case 'show':
if (!in_array(Input::get('id'), $root, true) || (Input::get('act') === 'delete' && !$this->User->hasAccess('delete', 'dlh_googlemapsp'))) {
System::getContainer()->get('monolog.logger.contao')->log(
LogLevel::ERROR,
'Not enough permissions to ' . Input::get('act') . ' Map ID "' . Input::get('id') . '"',
[
'contao' => new ContaoContext(__METHOD__, ContaoContext::ERROR),
],
);
$this->redirect('contao/main.php?act=error');
}
break;
case 'editAll':
case 'deleteAll':
case 'overrideAll':
$session = $this->Session->getData();
if (Input::get('act') === 'deleteAll' && !$this->User->hasAccess('delete', 'dlh_googlemapsp')) {
$session['CURRENT']['IDS'] = [];
} else {
$session['CURRENT']['IDS'] = array_intersect($session['CURRENT']['IDS'], $root);
}
$this->Session->setData($session);
break;
default:
if ((string) Input::get('act') !== '') {
System::getContainer()->get('monolog.logger.contao')->log(
LogLevel::ERROR,
'Not enough permissions to ' . Input::get('act') . ' Maps',
[
'contao' => new ContaoContext(__METHOD__, ContaoContext::ERROR),
],
);
$this->redirect('contao/main.php?act=error');
}
break;
}
This feature is available in Contao 4.7 and later.
This feature is available in Contao 4.10 and later.
This feature is available in Contao 4.12 and later.
This feature is available in Contao 5.0 and later.
class GoogleMapsAccessVoter extends AbstractDataContainerVoter
{
public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager)
{}
protected function getTable(): string
{
return 'tl_dlh_googlemaps';
}
protected function hasAccess(TokenInterface $token, CreateAction|DeleteAction|ReadAction|UpdateAction $action): bool
{
if (!$this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE . '.dlh_googlemaps'])) {
return false;
}
return match (true) {
$action instanceof CreateAction => $this->accessDecisionManager->decide($token, ['contao_user.dlh_googlemapsp'], 'create'),
$action instanceof ReadAction,
$action instanceof UpdateAction => $this->accessDecisionManager->decide($token, ['contao_user.dlh_googlemapss'], (int) $action->getCurrentId()),
$action instanceof DeleteAction => $this->accessDecisionManager->decide($token, ['contao_user.dlh_googlemapss'], (int) $action->getCurrentId())
&& $this->accessDecisionManager->decide($token, ['contao_user.dlh_googlemapsp'], 'delete'),
};
}
}
Jetzt erstmal löschen!
Es fehlen die Button-Callbacks ...
'label' => [
'fields' => ['title'],
'format' => '%s',
'label_callback' => ['tl_dlh_googlemaps', 'listRecords'],
],
'global_operations' => [
'all',
],
'operations' => [
'edit',
'children',
'copy',
'delete',
'show',
],
],
* Seit Contao 5.5 kann man die Defaults auch weglassen
Kaum löscht man paar Zeilen Code
Hat man das Feature implementiert
if (isset($GLOBALS['TL_HOOKS']['stylepicker4ward_getFilter']) && is_array($GLOBALS['TL_HOOKS']['stylepicker4ward_getFilter'])) {
foreach ($GLOBALS['TL_HOOKS']['stylepicker4ward_getFilter'] as $callback) {
System::importStatic($callback[0]);
$result = $this->{$callback[0]}->{$callback[1]}($table, $id);
if (is_array($result)) {
[$table, $layout, $section, $condition] = $result;
break;
}
}
}
$event = new GetStylePickerFilterEvent($table, (int) $id);
$this->eventDispatcher->dispatch($event);
$layout = $event->getLayout();
$section = $event->getSection();
$condition = $event->getCondition();
#[AsEventListener]
class StylePickerEventListener
{
public function __invoke(GetStylePickerFilterEvent $event): void
{
if ($event->getTable() !== 'my_table') {
return;
}
$event->setLayout(213);
$event->setCondition('foobar');
$event->setSection('your_section');
}
}
| Meldet Fehler | Fixed Fehler | |
|---|---|---|
| Coding standard | PHP_Codesniffer | PHP-CS-Fixer, ECS |
| Logik | PHPStan, Psalm | Rector |
Modifiziert Code und ist Token-Basiert (Lexing und Tokenisierung)
PHPToken oder `token_get_all`
$source = <<<'code'
T_OPEN_TAG
T_WHITESPACE
T_CLASS
T_WHITESPACE
T_STRING
T_CONST
T_WHITESPACE
T_STRING
T_LNUMBER
Basierend auf dem AST (Abstract Syntax Tree) -> Seit PHP 7
Geparsed über PHPParser
(by Nikita Popov / nikic)
return $this->foo && self::bar();```
PhpParser\Node\Stmt\Return_
└─ expr: PhpParser\Node\Expr\BinaryOp\BooleanAnd
├─ left: PhpParser\Node\Expr\PropertyFetch
| ├─ var: PhpParser\Node\Expr\Variable
| └─ name: PhpParser\Node\Identifier
└─ right: PhpParser\Node\Expr\StaticCall
├─ class: PhpParser\Node\Name
├─ name: PhpParser\Node\Identifier
└─ args: array()
RectorPHP
(by Tomas Votruba)
class Foo extends \Controller
{
public function bar()
{
$GLOBALS['TL_DCA']['tl_content']['palettes']['foo'] = '{foo_legend},foo;{bar_legend:hide},bar;{baz_legend:hide},baz';
}
}
Beispiel
Beispiel
return RectorConfig::configure()
->withPhpSets(
php83: true,
)
->withAttributesSets(
symfony: true,
doctrine: true,
)
->withSets([
ContaoLevelSetList::UP_TO_CONTAO_53,
ContaoSetList::ANNOTATIONS_TO_ATTRIBUTES,
])
->withPaths([
__DIR__ . '/contao',
__DIR__ . '/src',
])
->withImportNames(removeUnusedImports: true)
->withParallel()
;
Note to yourself, open the IDE