Did I tell you about this awesome, huge migration we just did? Sorry – still happy about it.
But a little story during the migration, we copied the whole staging database to the production system, everything worked fine, then we migrated the data and a few things got fucked up and unfortunately we don’t know why and due to the huge amount of data, no one wants to pay for it to know. Shop is running everything is fine.
All looks fine ?
One of the things which went wrong was, that right before we wanted to turn of maintenance mode and remove the password protection all categories showed a 404.
- URLs are in the seo_url table
- languages are configured
- sales_channel are active
- categories are active
- What other reasons come to your mind, which end in a 404 on a category?
APP_ENV=dev for the rescue
But I couldn’t find out what was wrong, so I installed all dev stuff on the production machine and turned on the dev mode. That helped, because the 404 was gone and I got a proper error message:
Page with id “b2f24f9294e14913a7bd9e00c354e15c” was not found.
Thanks to the symfony debug options, I got a stacktrace and the exception is thrown here
Shopware is using their API code to feed the Storefront
class CategoryException extends HttpException { [...] public static function pageNotFound(string $pageId): ShopwareHttpException { return new PageNotFoundException($pageId); } [...] }
To be fair, not very helpful. But … stacktrace, so lets go up one level:
// \Shopware\Core\Content\Category\SalesChannel\CategoryRoute::load #[Route(path: '/store-api/category/{navigationId}', name: 'store-api.category.detail', methods: ['GET', 'POST'])] public function load(string $navigationId, Request $request, SalesChannelContext $context): CategoryRouteResponse { if ($navigationId === self::HOME) { $navigationId = $context->getSalesChannel()->getNavigationCategoryId(); $request->attributes->set('navigationId', $navigationId); $routeParams = $request->attributes->get('_route_params', []); $routeParams['navigationId'] = $navigationId; $request->attributes->set('_route_params', $routeParams); } $category = $this->loadCategory($navigationId, $context); if (($category->getType() === CategoryDefinition::TYPE_FOLDER || $category->getType() === CategoryDefinition::TYPE_LINK) && $context->getSalesChannel()->getNavigationCategoryId() !== $navigationId ) { throw CategoryException::categoryNotFound($navigationId); } $pageId = $category->getCmsPageId(); $slotConfig = $category->getTranslation('slotConfig'); $salesChannel = $context->getSalesChannel(); if ($category->getId() === $salesChannel->getNavigationCategoryId() && $salesChannel->getHomeCmsPageId()) { $pageId = $salesChannel->getHomeCmsPageId(); $slotConfig = $salesChannel->getTranslation('homeSlotConfig'); } if (!$pageId) { return new CategoryRouteResponse($category); } $resolverContext = new EntityResolverContext($context, $request, $this->categoryDefinition, $category); $pages = $this->cmsPageLoader->load( $request, $this->createCriteria($pageId, $request), $context, $slotConfig, $resolverContext ); if (!$pages->has($pageId)) { throw CategoryException::pageNotFound($pageId); } /** @var CmsPageEntity $page */ $page = $pages->get($pageId); $category->setCmsPage($page); $category->setCmsPageId($pageId); return new CategoryRouteResponse($category); }
Did you know, that shopware is using their API routes internally to load categories? I’m sure they do that on other stuff as well. That is so smart, because all changed you do to the API is reflected on the frontend as well!
And also in case you don’t know, the so called “pageId” is the layout you attach a category to (or a product page).
So I checked the backend but the category doesn’t have a layout attached. I searched the complete codebase for the id but couldn’t find anything as well.
Where the fuck comes this id from!? ?
I started zgrep
-ping my database dumps, but they were so big, that I didn’t expect an answer soon. And because I’m stupid I wouldn’t have gotten an answer anyway: I forgot the -i
flag, so I wouldn’t find any UUID which are all exported in UPPERCASE ?.
So parallel to running zgrep
I looked through the code. It is again time for vardump/die debugging, because I didn’t have Xdebug on the production machine and expected, that it is faster to do this, than exporting the whole database and importing it locally. Beside the fact, that I try to avoid at nearly all costs to have any GDPR data in my machine (although encrypted and taken care of, but it gives me a bad feeling).
If not loaded – is must be a setter?
I assumed, that because the page id is not loaded with the category, somewhere a setPageId()
must be called and my assumption was rewarded, only two handful of results were shown. Looking through them, brought me quickly here:
\Shopware\Core\Content\Category\Subscriber\CategorySubscriber::entityLoaded
public function entityLoaded(EntityLoadedEvent $event): void { $salesChannelId = $event instanceof SalesChannelEntityLoadedEvent ? $event->getSalesChannelContext()->getSalesChannelId() : null; /** @var CategoryEntity $category */ foreach ($event->getEntities() as $category) { $categoryCmsPageId = $category->getCmsPageId(); // continue if cms page is given and was not set in the subscriber if ($categoryCmsPageId !== null && !$category->getCmsPageIdSwitched()) { continue; } // continue if cms page is given and not the overall default if ($categoryCmsPageId !== null && $categoryCmsPageId !== $this->systemConfigService->get(CategoryDefinition::CONFIG_KEY_DEFAULT_CMS_PAGE_CATEGORY)) { continue; } $userDefault = $this->systemConfigService->get(CategoryDefinition::CONFIG_KEY_DEFAULT_CMS_PAGE_CATEGORY, $salesChannelId); // cms page is not given in system config if ($userDefault === null) { continue; } /** @var string $userDefault */ $category->setCmsPageId($userDefault); // mark cms page as set in the subscriber $category->setCmsPageIdSwitched(true); } }
Mysterious config setting core.cms.default_category_cms_page
If we look through the method, we see, that CategoryDefinition::CONFIG_KEY_DEFAULT_CMS_PAGE_CATEGORY
is loaded from the config – never heard of it. The constants value is core.cms.default_category_cms_page
– never heard of that either.
If we search for the constant or the value, we find some JS files, but it seems the value can not be set via configuration.
A look into the database reveals our known value: {"_value": "b2f24f9294e14913a7bd9e00c354e15c"}
, ok at least now we know were this value comes from, and deducting from the name and the method code above it is the default layout.
We can have default values for layouts
How cool is that! I just learned, how to get rid of the write protected default layout for all categories WITHOUT attaching a layout to each and every category!
I checked the value on the staging server and it differed, so I copied the id over and we were back on track.
Why did a small amount of the categories loaded from the beginning, you ask? Because they had layouts attached.