• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

heimrichhannot / contao-utils-bundle / 13942910598

19 Mar 2025 09:16AM UTC coverage: 73.034% (-0.7%) from 73.745%
13942910598

Pull #93

github

koertho
updated tests
Pull Request #93: Forwardport #87

85 of 140 new or added lines in 6 files covered. (60.71%)

2 existing lines in 1 file now uncovered.

1040 of 1424 relevant lines covered (73.03%)

3.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

3.52
/src/Command/EntityFinderCommand.php
1
<?php
2

3
/*
4
 * Copyright (c) 2022 Heimrich & Hannot GmbH
5
 *
6
 * @license LGPL-3.0-or-later
7
 */
8

9
namespace HeimrichHannot\UtilsBundle\Command;
10

11
use Contao\ArticleModel;
12
use Contao\ContentModel;
13
use Contao\Controller;
14
use Contao\CoreBundle\Framework\ContaoFramework;
15
use Contao\Database;
16
use Contao\LayoutModel;
17
use Contao\ModuleModel;
18
use Contao\PageModel;
19
use Contao\ThemeModel;
20
use Doctrine\DBAL\Connection;
21
use HeimrichHannot\UtilsBundle\EntityFinder\EntityFinderHelper;
22
use HeimrichHannot\UtilsBundle\EntityFinder\Finder;
23
use HeimrichHannot\UtilsBundle\Event\ExtendEntityFinderEvent;
24
use Symfony\Component\Console\Attribute\AsCommand;
25
use Symfony\Component\Console\Command\Command;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
31

32
#[AsCommand(
33
    name: 'huh:utils:entity_finder',
34
    description: 'A command to find where an entity is included.'
35
)]
36
class EntityFinderCommand extends Command
37
{
38
    public function __construct(
39
        private ContaoFramework $contaoFramework,
40
        private EventDispatcherInterface $eventDispatcher,
41
        private Connection $connection,
42
        private EntityFinderHelper $entityFinderHelper,
43
        private readonly Finder $finder,
44
    )
45
    {
46
        parent::__construct();
1✔
47
    }
48

49
    protected function configure(): void
50
    {
51
        $this
1✔
52
            ->addArgument('table', InputArgument::REQUIRED, 'The database table')
1✔
53
            ->addArgument('id', InputArgument::REQUIRED, 'The entity id or alias (id is better supported).')
1✔
54
        ;
1✔
55
    }
56

57
    protected function execute(InputInterface $input, OutputInterface $output): int
58
    {
59
        $this->contaoFramework->initialize();
×
60
        $io = new SymfonyStyle($input, $output);
×
61

62
        $io->title('Find entity');
×
63

64
        if ($input->hasArgument('table') && $input->getArgument('table')) {
×
65
            $table = $input->getArgument('table');
×
66
        }
67

68
        if ($input->hasArgument('id') && $input->getArgument('id')) {
×
69
            $id = $input->getArgument('id');
×
70
        }
71

72
        $result = $this->loop($table, $id);
×
73
        $this->output($io, [$result]);
×
74
        $io->newLine();
×
75

76
        return 0;
×
77
    }
78

79
    private function loop(string $table, $id): array
80
    {
81
        $current = [
×
82
            'table' => $table,
×
83
            'id' => $id,
×
84
            'parents' => [],
×
85
        ];
×
86

87
        $parents = [];
×
88

89
        $this->findEntity($table, $id, $parents);
×
NEW
90
        $event = $this->runLegacyExtendEntityFinderEvent($table, $id, $parents);
×
91

92
        $this->findInserttags($event);
×
93

94
        $cache = [];
×
95

96
        foreach ($event->getParents() as $parent) {
×
97
            if (!isset($parent['table']) || !isset($parent['id'])) {
×
98
                continue;
×
99
            }
100
            $cacheKey = $parent['table'].'_'.$parent['id'];
×
101

102
            if (\in_array($cacheKey, $cache)) {
×
103
                continue;
×
104
            }
105
            $cache[] = $cacheKey;
×
106
            $current['parents'][] = $this->loop($parent['table'], $parent['id']);
×
107
        }
108

109
        return $current;
×
110
    }
111

112
    private function output(SymfonyStyle $io, array $tree, string $prepend = '', int $depth = 0): void
113
    {
114
        $itemCount = \count($tree);
×
115
        $i = 0;
×
116

117
        foreach ($tree as $item) {
×
118
            ++$i;
×
119

120
            if ($depth > 0) {
×
121
                if ($i === $itemCount) {
×
122
                    $newPrepend = $prepend.'└── ';
×
123
                    $nextPrepend = $prepend.'    ';
×
124
                } else {
125
                    $newPrepend = $prepend.'├── ';
×
126
                    $nextPrepend = $prepend.'│   ';
×
127
                }
128
            } else {
129
                $newPrepend = $prepend;
×
130
                $nextPrepend = $prepend;
×
131
            }
132
            $io->writeln($newPrepend.$this->createText($item['table'], $item['id']));
×
133

134
            if ($item['parents'] ?? false) {
×
135
                $this->output($io, $item['parents'], $nextPrepend, ++$depth);
×
136
            }
137
        }
138
    }
139

140
    private function findEntity(string $table, $id, array &$parents, bool $onlyText = false): ?string
141
    {
142
        Controller::loadLanguageFile('default');
×
143

144
        switch ($table) {
145
            case ContentModel::getTable():
×
146
                $element = ContentModel::findByIdOrAlias($id);
×
147

148
                if ($element) {
×
149
                    $parents[] = ['table' => $element->ptable, 'id' => $element->pid];
×
150

151
                    return 'Content Element: '.($GLOBALS['TL_LANG']['CTE'][$element->type][0] ?? $element->type).' (ID: '.$element->id.', Type: '.$element->type.')';
×
152
                }
153

154
                return 'Content Element not found: ID '.$id;
×
155

156
            case ArticleModel::getTable():
×
157
                $element = ArticleModel::findByPk($id);
×
158

159
                if ($element) {
×
160
                    $parents[] = ['table' => PageModel::getTable(), 'id' => $element->pid];
×
161

162
                    if (!$onlyText) {
×
163
                        foreach ($this->entityFinderHelper->findModulesByInserttag('html', 'html', 'insert_article', $element->id) as $id) {
×
164
                            $parents[] = ['table' => ModuleModel::getTable(), 'id' => $id];
×
165
                        }
166
                        foreach ($this->entityFinderHelper->findContentElementByInserttag('html', 'html', 'insert_article', $element->id) as $id) {
×
167
                            $parents[] = ['table' => ContentModel::getTable(), 'id' => $id];
×
168
                        }
169
                    }
170

171
                    return 'Article: '.$element->title.' (ID: '.$element->id.')';
×
172
                }
173

174
                return 'Article not found: ID '.$id;
×
175

176
            case ModuleModel::getTable():
×
177
                if ($onlyText) {
×
178
                    Controller::loadLanguageFile('modules');
×
179
                }
180
                $element = ModuleModel::findByIdOrAlias($id);
×
181

182
                if ($element) {
×
183
                    if (!$onlyText) {
×
184
                        $this->findFrontendModuleParents($element, $parents, $id);
×
185
                    }
186

187
                    return 'Frontend module: '.($GLOBALS['TL_LANG']['FMD'][$element->type][0] ?? $element->type).' (ID: '.$element->id.', Type: '.$element->type.')';
×
188
                }
189

190
                return 'Frontend module not found: ID '.$id;
×
191

192
            case LayoutModel::getTable():
×
193
                $layout = LayoutModel::findById($id);
×
194

195
                if ($layout) {
×
196
                    $parents[] = ['table' => ThemeModel::getTable(), 'id' => $layout->pid];
×
197

198
                    return 'Layout: '.html_entity_decode($layout->name).' (ID: '.$layout->id.')';
×
199
                }
200

201
                return 'Layout not found: ID '.$id;
×
202

203
            case ThemeModel::getTable():
×
204
                $theme = ThemeModel::findByPk($id);
×
205

206
                if ($theme) {
×
207
                    return '<options=bold>Theme: '.$theme->name.'</> (ID: '.$theme->id.')';
×
208
                }
209

210
                return 'Theme not found: ID '.$id;
×
211

212
            case PageModel::getTable():
×
213
                $page = PageModel::findByPk($id);
×
214

215
                if ($page) {
×
216
                    return '<options=bold>Page: '.$page->title.'</> (ID: '.$page->id.', Type: '.$page->type.', DNS: '.$page->getFrontendUrl().' )';
×
217
                }
218

219
                return 'Page not found: ID '.$id;
×
220
        }
221

NEW
222
        $element = $this->finder->find($table, $id);
×
NEW
223
        if ($element) {
×
NEW
224
            if ($onlyText) {
×
NEW
225
                return $element->description;
×
226
            }
NEW
227
            if (null === $element->parents) {
×
NEW
228
                return null;
×
229
            }
NEW
230
            foreach ($element->getParents()($element->table, $element->id) as $parent) {
×
NEW
231
                $parents[] = ['table' => $parent['table'], 'id' => $parent['id']];
×
232
            }
NEW
233
            return null;
×
234
        }
235

UNCOV
236
        return null;
×
237
    }
238

239
    private function createText(string $table, $id): string
240
    {
241
        $parents = [];
×
242

243
        if ($text = $this->findEntity($table, $id, $parents, true)) {
×
244
            return $text;
×
245
        }
246

247
        /** @var ExtendEntityFinderEvent $event */
NEW
248
        $event = $this->runLegacyExtendEntityFinderEvent($table, $id, [], true);
×
249

250
        if ($event->getOutput()) {
×
251
            return $event->getOutput();
×
252
        }
253

254
        return 'Unsupported entity: '.$table.' (ID: '.$id.')';
×
255
    }
256

257
    private function runLegacyExtendEntityFinderEvent(string $table, $id, array $parents, bool $onlyText = false): ExtendEntityFinderEvent
258
    {
259

NEW
260
        if ($this->eventDispatcher->hasListeners(ExtendEntityFinderEvent::class)) {
×
NEW
261
            trigger_deprecation(
×
NEW
262
                'heimrichhannot/contao-utils-bundle',
×
NEW
263
                '3.7',
×
NEW
264
                'Using the ExtendEntityFinderEvent is deprecated. Use EntityFinderFindEvent instead.'
×
UNCOV
265
            );
×
266
        }
NEW
267
        $event = $this->eventDispatcher->dispatch(
×
NEW
268
            new ExtendEntityFinderEvent($table, $id, $parents, [], $this->entityFinderHelper, $onlyText),
×
NEW
269
        );
×
270

271
        return $event;
×
272
    }
273

274
    private function findFrontendModuleParents(ModuleModel $module, array &$parents): void
275
    {
276
        $contentelements = ContentModel::findBy(['tl_content.type=?', 'tl_content.module=?'], ['module', $module->id]);
×
277

278
        if ($contentelements) {
×
279
            foreach ($contentelements as $contentelement) {
×
280
                $parents[] = ['table' => ContentModel::getTable(), 'id' => $contentelement->id];
×
281
            }
282
        }
283

284
        $result = Database::getInstance()
×
285
            ->prepare("SELECT id FROM tl_layout WHERE modules LIKE '%:\"".(string) ((int) $module->id)."\"%'")
×
286
            ->execute();
×
287

288
        foreach ($result->fetchEach('id') as $layoutId) {
×
289
            $parents[] = ['table' => LayoutModel::getTable(), 'id' => $layoutId];
×
290
        }
291

292
        $result = Database::getInstance()
×
293
            ->prepare("SELECT id FROM tl_module
×
294
                        WHERE type='html'
295
                        AND (
296
                            html LIKE '%{{insert_module::".$module->id."}}%'
×
297
                            OR html LIKE '%{{insert_module::".$module->id."::%')")
×
298
            ->execute();
×
299

300
        foreach ($result->fetchEach('id') as $moduleId) {
×
301
            $parents[] = ['table' => ModuleModel::getTable(), 'id' => $moduleId];
×
302
        }
303
    }
304

305
    private function findInserttags(ExtendEntityFinderEvent $event): void
306
    {
307
        $stmt = $this->connection->prepare(
×
308
            "SELECT id FROM tl_module WHERE type='html' AND html LIKE ?");
×
309

310
        foreach ($event->getInserttags() as $inserttag) {
×
311
            $result = $stmt->executeQuery(['%'.$inserttag.'%']);
×
312

313
            foreach ($result->fetchAllAssociative() as $row) {
×
314
                $event->addParent(ModuleModel::getTable(), $row['id']);
×
315
            }
316
        }
317
    }
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc