1 | <?php |
||||||
2 | /** |
||||||
3 | * SEOmatic plugin for Craft CMS |
||||||
4 | * |
||||||
5 | * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful, |
||||||
6 | * and flexible |
||||||
7 | * |
||||||
8 | * @link https://nystudio107.com |
||||||
9 | * @copyright Copyright (c) 2019 nystudio107 |
||||||
10 | */ |
||||||
11 | |||||||
12 | namespace nystudio107\seomatic\seoelements; |
||||||
13 | |||||||
14 | use Craft; |
||||||
15 | use craft\base\ElementInterface; |
||||||
0 ignored issues
–
show
|
|||||||
16 | use craft\base\Model; |
||||||
17 | use craft\elements\db\ElementQueryInterface; |
||||||
18 | use craft\elements\Entry; |
||||||
19 | use craft\events\DefineHtmlEvent; |
||||||
20 | use craft\events\SectionEvent; |
||||||
21 | use craft\gql\interfaces\elements\Entry as EntryInterface; |
||||||
22 | use craft\helpers\ElementHelper; |
||||||
23 | use craft\models\Section; |
||||||
24 | use craft\models\Site; |
||||||
25 | use craft\services\Entries; |
||||||
0 ignored issues
–
show
The type
craft\services\Entries was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
26 | use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset; |
||||||
27 | use nystudio107\seomatic\base\GqlSeoElementInterface; |
||||||
28 | use nystudio107\seomatic\base\SeoElementInterface; |
||||||
29 | use nystudio107\seomatic\helpers\ArrayHelper; |
||||||
30 | use nystudio107\seomatic\helpers\Config as ConfigHelper; |
||||||
31 | use nystudio107\seomatic\helpers\PluginTemplate; |
||||||
32 | use nystudio107\seomatic\models\MetaBundle; |
||||||
33 | use nystudio107\seomatic\Seomatic; |
||||||
34 | use yii\base\Event; |
||||||
35 | use yii\base\InvalidConfigException; |
||||||
36 | |||||||
37 | /** |
||||||
38 | * @author nystudio107 |
||||||
39 | * @package Seomatic |
||||||
40 | * @since 3.2.0 |
||||||
41 | */ |
||||||
42 | class SeoEntry implements SeoElementInterface, GqlSeoElementInterface |
||||||
43 | { |
||||||
44 | // Constants |
||||||
45 | // ========================================================================= |
||||||
46 | |||||||
47 | public const META_BUNDLE_TYPE = 'section'; |
||||||
48 | public const ELEMENT_CLASSES = [ |
||||||
49 | Entry::class, |
||||||
50 | ]; |
||||||
51 | public const REQUIRED_PLUGIN_HANDLE = null; |
||||||
52 | public const CONFIG_FILE_PATH = 'entrymeta/Bundle'; |
||||||
53 | |||||||
54 | // Public Static Methods |
||||||
55 | // ========================================================================= |
||||||
56 | |||||||
57 | /** |
||||||
58 | * Return the sourceBundleType for that this SeoElement handles |
||||||
59 | * |
||||||
60 | * @return string |
||||||
61 | */ |
||||||
62 | public static function getMetaBundleType(): string |
||||||
63 | { |
||||||
64 | return self::META_BUNDLE_TYPE; |
||||||
65 | } |
||||||
66 | |||||||
67 | /** |
||||||
68 | * Returns an array of the element classes that are handled by this SeoElement |
||||||
69 | * |
||||||
70 | * @return array |
||||||
71 | */ |
||||||
72 | public static function getElementClasses(): array |
||||||
73 | { |
||||||
74 | return self::ELEMENT_CLASSES; |
||||||
75 | } |
||||||
76 | |||||||
77 | /** |
||||||
78 | * Return the refHandle (e.g.: `entry` or `category`) for the SeoElement |
||||||
79 | * |
||||||
80 | * @return string |
||||||
81 | */ |
||||||
82 | public static function getElementRefHandle(): string |
||||||
83 | { |
||||||
84 | return Entry::refHandle() ?? 'entry'; |
||||||
85 | } |
||||||
86 | |||||||
87 | /** |
||||||
88 | * Return the handle to a required plugin for this SeoElement type |
||||||
89 | * |
||||||
90 | * @return null|string |
||||||
91 | */ |
||||||
92 | public static function getRequiredPluginHandle() |
||||||
93 | { |
||||||
94 | return self::REQUIRED_PLUGIN_HANDLE; |
||||||
95 | } |
||||||
96 | |||||||
97 | /** |
||||||
98 | * Install any event handlers for this SeoElement type |
||||||
99 | */ |
||||||
100 | public static function installEventHandlers() |
||||||
101 | { |
||||||
102 | $request = Craft::$app->getRequest(); |
||||||
103 | |||||||
104 | // Install for all requests |
||||||
105 | Event::on( |
||||||
106 | Entries::class, |
||||||
107 | Entries::EVENT_AFTER_SAVE_SECTION, |
||||||
108 | function(SectionEvent $event) { |
||||||
0 ignored issues
–
show
The parameter
$event is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||||
109 | Craft::debug( |
||||||
110 | 'Entries::EVENT_AFTER_SAVE_SECTION', |
||||||
111 | __METHOD__ |
||||||
112 | ); |
||||||
113 | Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE); |
||||||
114 | } |
||||||
115 | ); |
||||||
116 | Event::on( |
||||||
117 | Entries::class, |
||||||
118 | Entries::EVENT_AFTER_DELETE_SECTION, |
||||||
119 | function(SectionEvent $event) { |
||||||
0 ignored issues
–
show
The parameter
$event is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||||
120 | Craft::debug( |
||||||
121 | 'Entries::EVENT_AFTER_DELETE_SECTION', |
||||||
122 | __METHOD__ |
||||||
123 | ); |
||||||
124 | Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE); |
||||||
125 | } |
||||||
126 | ); |
||||||
127 | |||||||
128 | // Install for all non-console requests |
||||||
129 | if (!$request->getIsConsoleRequest()) { |
||||||
130 | // Handler: Entries::EVENT_AFTER_SAVE_SECTION |
||||||
131 | Event::on( |
||||||
132 | Entries::class, |
||||||
133 | Entries::EVENT_AFTER_SAVE_SECTION, |
||||||
134 | function(SectionEvent $event) { |
||||||
135 | Craft::debug( |
||||||
136 | 'Entries::EVENT_AFTER_SAVE_SECTION', |
||||||
137 | __METHOD__ |
||||||
138 | ); |
||||||
139 | if ($event->section !== null && $event->section->id !== null) { |
||||||
140 | Seomatic::$plugin->metaBundles->invalidateMetaBundleById( |
||||||
141 | SeoEntry::getMetaBundleType(), |
||||||
142 | $event->section->id, |
||||||
143 | $event->isNew |
||||||
144 | ); |
||||||
145 | // Create the meta bundles for this section if it's new |
||||||
146 | if ($event->isNew) { |
||||||
147 | SeoEntry::createContentMetaBundle($event->section); |
||||||
148 | Seomatic::$plugin->sitemaps->submitSitemapIndex(); |
||||||
149 | } |
||||||
150 | } |
||||||
151 | } |
||||||
152 | ); |
||||||
153 | // Handler: Entries::EVENT_AFTER_DELETE_SECTION |
||||||
154 | Event::on( |
||||||
155 | Entries::class, |
||||||
156 | Entries::EVENT_AFTER_DELETE_SECTION, |
||||||
157 | function(SectionEvent $event) { |
||||||
158 | Craft::debug( |
||||||
159 | 'Entries::EVENT_AFTER_DELETE_SECTION', |
||||||
160 | __METHOD__ |
||||||
161 | ); |
||||||
162 | if ($event->section !== null && $event->section->id !== null) { |
||||||
163 | Seomatic::$plugin->metaBundles->invalidateMetaBundleById( |
||||||
164 | SeoEntry::getMetaBundleType(), |
||||||
165 | $event->section->id, |
||||||
166 | false |
||||||
167 | ); |
||||||
168 | // Delete the meta bundles for this section |
||||||
169 | Seomatic::$plugin->metaBundles->deleteMetaBundleBySourceId( |
||||||
170 | SeoEntry::getMetaBundleType(), |
||||||
171 | $event->section->id |
||||||
172 | ); |
||||||
173 | } |
||||||
174 | } |
||||||
175 | ); |
||||||
176 | } |
||||||
177 | |||||||
178 | // Install only for non-console site requests |
||||||
179 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { |
||||||
180 | } |
||||||
181 | |||||||
182 | // Handler: Entry::EVENT_DEFINE_SIDEBAR_HTML |
||||||
183 | Event::on( |
||||||
184 | Entry::class, |
||||||
185 | Entry::EVENT_DEFINE_SIDEBAR_HTML, |
||||||
186 | static function(DefineHtmlEvent $event) { |
||||||
187 | Craft::debug( |
||||||
188 | 'Entry::EVENT_DEFINE_SIDEBAR_HTML', |
||||||
189 | __METHOD__ |
||||||
190 | ); |
||||||
191 | $html = ''; |
||||||
192 | Seomatic::$view->registerAssetBundle(SeomaticAsset::class); |
||||||
0 ignored issues
–
show
The method
registerAssetBundle() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
193 | /** @var Entry $entry */ |
||||||
194 | $entry = $event->sender ?? null; |
||||||
195 | if ($entry !== null && $entry->uri !== null) { |
||||||
196 | Seomatic::$plugin->metaContainers->previewMetaContainers($entry->uri, $entry->siteId, true, true, $entry); |
||||||
197 | // Render our preview sidebar template |
||||||
198 | if (Seomatic::$settings->displayPreviewSidebar && Seomatic::$matchedElement) { |
||||||
199 | $html .= PluginTemplate::renderPluginTemplate('_sidebars/entry-preview.twig'); |
||||||
200 | } |
||||||
201 | // Render our analysis sidebar template |
||||||
202 | // @TODO: This will be added an upcoming 'pro' edition |
||||||
203 | // if (Seomatic::$settings->displayAnalysisSidebar && Seomatic::$matchedElement) { |
||||||
204 | // $html .= PluginTemplate::renderPluginTemplate('_sidebars/entry-analysis.twig'); |
||||||
205 | // } |
||||||
206 | } |
||||||
207 | $event->html .= $html; |
||||||
208 | } |
||||||
209 | ); |
||||||
210 | } |
||||||
211 | |||||||
212 | /** |
||||||
213 | * Return an ElementQuery for the sitemap elements for the given MetaBundle |
||||||
214 | * |
||||||
215 | * @param MetaBundle $metaBundle |
||||||
216 | * |
||||||
217 | * @return ElementQueryInterface |
||||||
218 | */ |
||||||
219 | public static function sitemapElementsQuery(MetaBundle $metaBundle): ElementQueryInterface |
||||||
220 | { |
||||||
221 | $query = Entry::find() |
||||||
222 | ->section($metaBundle->sourceHandle) |
||||||
223 | ->siteId($metaBundle->sourceSiteId) |
||||||
224 | ->limit($metaBundle->metaSitemapVars->sitemapLimit); |
||||||
225 | if ($metaBundle->sourceType === 'structure' |
||||||
226 | && !empty($metaBundle->metaSitemapVars->structureDepth)) { |
||||||
227 | $query->level('<=' . $metaBundle->metaSitemapVars->structureDepth); |
||||||
228 | } |
||||||
229 | |||||||
230 | return $query; |
||||||
231 | } |
||||||
232 | |||||||
233 | /** |
||||||
234 | * Return an ElementInterface for the sitemap alt element for the given MetaBundle |
||||||
235 | * and Element ID |
||||||
236 | * |
||||||
237 | * @param MetaBundle $metaBundle |
||||||
238 | * @param int $elementId |
||||||
239 | * @param int $siteId |
||||||
240 | * |
||||||
241 | * @return null|ElementInterface |
||||||
242 | */ |
||||||
243 | public static function sitemapAltElement( |
||||||
244 | MetaBundle $metaBundle, |
||||||
245 | int $elementId, |
||||||
246 | int $siteId, |
||||||
247 | ) { |
||||||
248 | return Entry::find() |
||||||
249 | ->section($metaBundle->sourceHandle) |
||||||
250 | ->id($elementId) |
||||||
251 | ->siteId($siteId) |
||||||
252 | ->limit(1) |
||||||
253 | ->one(); |
||||||
254 | } |
||||||
255 | |||||||
256 | /** |
||||||
257 | * Return a preview URI for a given $sourceHandle and $siteId |
||||||
258 | * This just returns the first element |
||||||
259 | * |
||||||
260 | * @param string $sourceHandle |
||||||
261 | * @param int|null $siteId |
||||||
262 | * @param int|string|null $typeId |
||||||
263 | * |
||||||
264 | * @return ?string |
||||||
265 | */ |
||||||
266 | public static function previewUri(string $sourceHandle, $siteId, $typeId = null): ?string |
||||||
267 | { |
||||||
268 | $uri = null; |
||||||
269 | $query = Entry::find() |
||||||
270 | ->section($sourceHandle) |
||||||
271 | ->siteId($siteId); |
||||||
272 | if (!empty($typeId)) { |
||||||
273 | $query |
||||||
274 | ->andWhere([ |
||||||
275 | 'typeId' => (int)$typeId, |
||||||
276 | ]); |
||||||
277 | } |
||||||
278 | $element = $query->one(); |
||||||
279 | if ($element) { |
||||||
280 | $uri = $element->uri; |
||||||
281 | } |
||||||
282 | |||||||
283 | return $uri; |
||||||
284 | } |
||||||
285 | |||||||
286 | /** |
||||||
287 | * Return an array of FieldLayouts from the $sourceHandle |
||||||
288 | * |
||||||
289 | * @param string $sourceHandle |
||||||
290 | * @param int|string|null $typeId |
||||||
291 | * |
||||||
292 | * @return array |
||||||
293 | */ |
||||||
294 | public static function fieldLayouts(string $sourceHandle, $typeId = null): array |
||||||
295 | { |
||||||
296 | $layouts = []; |
||||||
297 | $section = Craft::$app->getEntries()->getSectionByHandle($sourceHandle); |
||||||
298 | if ($section) { |
||||||
299 | $entryTypes = $section->getEntryTypes(); |
||||||
300 | foreach ($entryTypes as $entryType) { |
||||||
301 | if ($entryType->fieldLayoutId && ($entryType->id == $typeId || empty($typeId))) { |
||||||
302 | $layouts[] = Craft::$app->getFields()->getLayoutById($entryType->fieldLayoutId); |
||||||
303 | } |
||||||
304 | } |
||||||
305 | } |
||||||
306 | |||||||
307 | return $layouts; |
||||||
308 | } |
||||||
309 | |||||||
310 | /** |
||||||
311 | * Return the (entry) type menu as a $id => $name associative array |
||||||
312 | * |
||||||
313 | * @param string $sourceHandle |
||||||
314 | * |
||||||
315 | * @return array |
||||||
316 | */ |
||||||
317 | public static function typeMenuFromHandle(string $sourceHandle): array |
||||||
318 | { |
||||||
319 | $typeMenu = []; |
||||||
320 | |||||||
321 | $section = self::sourceModelFromHandle($sourceHandle); |
||||||
322 | if ($section !== null) { |
||||||
323 | $entryTypes = $section->getEntryTypes(); |
||||||
324 | foreach ($entryTypes as $entryType) { |
||||||
325 | $typeMenu[$entryType->id] = $entryType->name; |
||||||
326 | } |
||||||
327 | } |
||||||
328 | |||||||
329 | return $typeMenu; |
||||||
330 | } |
||||||
331 | |||||||
332 | /** |
||||||
333 | * Return the source model of the given $sourceId |
||||||
334 | * |
||||||
335 | * @param int $sourceId |
||||||
336 | * |
||||||
337 | * @return Section|null |
||||||
338 | */ |
||||||
339 | public static function sourceModelFromId(int $sourceId) |
||||||
340 | { |
||||||
341 | return Craft::$app->getEntries()->getSectionById($sourceId); |
||||||
342 | } |
||||||
343 | |||||||
344 | /** |
||||||
345 | * Return the source model of the given $sourceId |
||||||
346 | * |
||||||
347 | * @param string $sourceHandle |
||||||
348 | * |
||||||
349 | * @return Section|null |
||||||
350 | */ |
||||||
351 | public static function sourceModelFromHandle(string $sourceHandle) |
||||||
352 | { |
||||||
353 | return Craft::$app->getEntries()->getSectionByHandle($sourceHandle); |
||||||
354 | } |
||||||
355 | |||||||
356 | /** |
||||||
357 | * Return the most recently updated Element from a given source model |
||||||
358 | * |
||||||
359 | * @param Model $sourceModel |
||||||
360 | * @param int $sourceSiteId |
||||||
361 | * |
||||||
362 | * @return null|ElementInterface |
||||||
363 | */ |
||||||
364 | public static function mostRecentElement(Model $sourceModel, int $sourceSiteId) |
||||||
365 | { |
||||||
366 | /** @var Section $sourceModel */ |
||||||
367 | return Entry::find() |
||||||
368 | ->section($sourceModel->handle) |
||||||
369 | ->siteId($sourceSiteId) |
||||||
370 | ->limit(1) |
||||||
371 | ->orderBy(['elements.dateUpdated' => SORT_DESC]) |
||||||
372 | ->one(); |
||||||
373 | } |
||||||
374 | |||||||
375 | /** |
||||||
376 | * Return the path to the config file directory |
||||||
377 | * |
||||||
378 | * @return string |
||||||
379 | */ |
||||||
380 | public static function configFilePath(): string |
||||||
381 | { |
||||||
382 | return self::CONFIG_FILE_PATH; |
||||||
383 | } |
||||||
384 | |||||||
385 | /** |
||||||
386 | * Return a meta bundle config array for the given $sourceModel |
||||||
387 | * |
||||||
388 | * @param Model $sourceModel |
||||||
389 | * |
||||||
390 | * @return array |
||||||
391 | */ |
||||||
392 | public static function metaBundleConfig(Model $sourceModel): array |
||||||
393 | { |
||||||
394 | /** @var Section $sourceModel */ |
||||||
395 | return ArrayHelper::merge( |
||||||
396 | ConfigHelper::getConfigFromFile(self::configFilePath()), |
||||||
397 | [ |
||||||
398 | 'sourceId' => $sourceModel->id, |
||||||
399 | 'sourceName' => (string)$sourceModel->name, |
||||||
400 | 'sourceHandle' => $sourceModel->handle, |
||||||
401 | 'sourceType' => $sourceModel->type, |
||||||
402 | ] |
||||||
403 | ); |
||||||
404 | } |
||||||
405 | |||||||
406 | /** |
||||||
407 | * Return the source id from the $element |
||||||
408 | * |
||||||
409 | * @param ElementInterface $element |
||||||
410 | * |
||||||
411 | * @return int|null |
||||||
412 | */ |
||||||
413 | public static function sourceIdFromElement(ElementInterface $element) |
||||||
414 | { |
||||||
415 | // Get the root element so we handle nested matrix entries |
||||||
416 | $rootElement = ElementHelper::rootElement($element); |
||||||
0 ignored issues
–
show
The function
craft\helpers\ElementHelper::rootElement() has been deprecated: in 5.4.0. Use [[ElementInterface::getRootOwner()]] instead.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||||
417 | if ($rootElement instanceof Entry) { |
||||||
418 | return $rootElement->sectionId; |
||||||
419 | } |
||||||
420 | // If the root element isn't an entry, handle that case too |
||||||
421 | $sourceBundleType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($rootElement); |
||||||
422 | if ($sourceBundleType !== null) { |
||||||
423 | $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType); |
||||||
424 | if ($seoElement !== null) { |
||||||
425 | return $seoElement::sourceIdFromElement($rootElement); |
||||||
426 | } |
||||||
427 | } |
||||||
428 | |||||||
429 | return null; |
||||||
430 | } |
||||||
431 | |||||||
432 | /** |
||||||
433 | * Return the (entry) type id from the $element |
||||||
434 | * |
||||||
435 | * @param ElementInterface $element |
||||||
436 | * |
||||||
437 | * @return int|null |
||||||
438 | */ |
||||||
439 | public static function typeIdFromElement(ElementInterface $element) |
||||||
440 | { |
||||||
441 | /** @var Entry $element */ |
||||||
442 | return $element->typeId; |
||||||
443 | } |
||||||
444 | |||||||
445 | /** |
||||||
446 | * Return the source handle from the $element |
||||||
447 | * |
||||||
448 | * @param ElementInterface $element |
||||||
449 | * |
||||||
450 | * @return string|null |
||||||
451 | */ |
||||||
452 | public static function sourceHandleFromElement(ElementInterface $element) |
||||||
453 | { |
||||||
454 | $sourceHandle = ''; |
||||||
0 ignored issues
–
show
|
|||||||
455 | /** @var Entry $element */ |
||||||
456 | try { |
||||||
457 | $sourceHandle = $element->getSection()?->handle; |
||||||
458 | } catch (InvalidConfigException $e) { |
||||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||||||
459 | } |
||||||
460 | |||||||
461 | return $sourceHandle; |
||||||
462 | } |
||||||
463 | |||||||
464 | /** |
||||||
465 | * Create a MetaBundle in the db for each site, from the passed in $sourceModel |
||||||
466 | * |
||||||
467 | * @param Model $sourceModel |
||||||
468 | */ |
||||||
469 | public static function createContentMetaBundle(Model $sourceModel) |
||||||
470 | { |
||||||
471 | /** @var Section $sourceModel */ |
||||||
472 | $sites = Craft::$app->getSites()->getAllSites(); |
||||||
473 | /** @var Site $site */ |
||||||
474 | foreach ($sites as $site) { |
||||||
475 | $seoElement = self::class; |
||||||
476 | Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id, null, true); |
||||||
477 | } |
||||||
478 | } |
||||||
479 | |||||||
480 | /** |
||||||
481 | * Create all the MetaBundles in the db for this Seo Element |
||||||
482 | */ |
||||||
483 | public static function createAllContentMetaBundles() |
||||||
484 | { |
||||||
485 | // Get all of the sections with URLs |
||||||
486 | $sections = Craft::$app->getEntries()->getAllSections(); |
||||||
487 | foreach ($sections as $section) { |
||||||
488 | self::createContentMetaBundle($section); |
||||||
489 | } |
||||||
490 | } |
||||||
491 | |||||||
492 | /** |
||||||
493 | * @inheritdoc |
||||||
494 | */ |
||||||
495 | public static function getGqlInterfaceTypeName() |
||||||
496 | { |
||||||
497 | return EntryInterface::getName(); |
||||||
498 | } |
||||||
499 | } |
||||||
500 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths