定制 reessolutions/module-worker-mode 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

reessolutions/module-worker-mode

Composer 安装命令:

composer require reessolutions/module-worker-mode

包简介

FrankenPHP worker-mode compatibility fixes for Reessolutions projects

README 文档

README

Makes Magento / MageOS run correctly under FrankenPHP worker mode, where a single PHP process handles many requests in sequence without restarting between them.

The problem

Standard PHP-FPM restarts the PHP process after every request. Singletons registered in the DI container are destroyed and rebuilt fresh. In FrankenPHP worker mode the process is persistent — singletons live for the lifetime of the worker, and state left behind by request N bleeds into request N+1.

The opengento/module-application package provides the scaffolding for this (multi-area BootstrapPool, session registry, a Resetter that calls _resetState() between requests). This module provides all the Magento-specific fixes that opengento does not cover.

Key concepts

ResetAfterRequestInterface

When a class implements Magento\Framework\ObjectManager\ResetAfterRequestInterface, the opengento Resetter calls _resetState() on it as a direct method call after every request. This is how all stateful singletons in this module reset themselves.

The PHP 8.4 lazy ghost problem

For classes that do not implement ResetAfterRequestInterface, the Resetter falls back to resetting their properties via ReflectionProperty::setValue() — entries in etc/reset.json. On PHP 8.4, the DI container creates Interceptors as lazy ghosts (ReflectionClass::newLazyGhost()). The Resetter holds a ReflectionProperty sourced from the parent class. On an uninitialized ghost, setValue() from the parent's RP writes into the parent scope, but running code reads the Interceptor scope (uninitialized), which triggers the lazy initialiser, overwriting the reset.

The fix: subclass the problem class and implement ResetAfterRequestInterface. The Resetter then calls _resetState() as a method, which initialises the ghost before writing — so the reset lands in the correct live property slot.

This module uses this pattern for Layout, Page\Config, and Design. These three are responsible for the worst worker-mode bugs (wrong area CSS, blank pages, stale layout XML).

What each piece fixes

Session name contamination — Session/FrontendConfig

After an admin request on a worker, ini 'session.name' is 'admin' for that PHP process. A subsequent frontend CustomerSession::start() calls session_start() and reads $_COOKIE['admin'] instead of $_COOKIE['PHPSESSID']. This loads the admin session as the customer session (corrupting both), or loads nothing at all for guests.

FrontendConfig stores session.name = 'PHPSESSID' in $options so initIniOptions() always calls ini_set('session.name', 'PHPSESSID') before session_start(), resetting any contamination from a prior admin request. Injected as sessionConfig for CustomerSession and CheckoutSession in frontend and webapi_rest di.xml.

Session accumulation — App/Session/SessionRegistry

The base SessionRegistry holds a WeakMap of active sessions. Our subclass clears the WeakMap in _resetState(). Without this, startSessions() at the top of each new request would call start() on sessions from every previous request on this worker, including sessions from the wrong area (e.g. starting CustomerSession in an admin request with frontend cookie config, corrupting admin login).

Object manager context — Plugin/App/ObjectManagerContextPlugin

ObjectManager::$_instance is a static property overwritten by every new OM constructor. After the first webapi_rest or adminhtml bootstrap, getInstance() returns the wrong area's OM for frontend requests. This plugin restores $_instance to the correct area's OM before each request.

Area design reset — Model/App/Area

After the first request, _loadedParts['design'] = true causes load(PART_DESIGN) to be a no-op. Since Design._resetState() clears _area and _theme, the Area must re-run _initDesign() to call setArea() and setDefaultDesignTheme() and restore the correct theme. Our subclass clears _loadedParts['design'] in _resetState().

Design area — Model/View/Design

Subclasses Magento\Theme\Model\View\Design and implements ResetAfterRequestInterface. _resetState() clears _area and _theme. Fixes the PHP 8.4 lazy ghost reset failure described above — without this, _area persists as 'webapi_rest' across requests and frontend pages render with API-area CSS URLs and blank bodies.

Layout state — Model/View/Layout

Subclasses Magento\Framework\View\Layout and implements ResetAfterRequestInterface. _resetState() clears _xml, _blocks, readerContext, and all other per-request layout state.

The _xml field is the most critical: without it, stale merged layout XML from the previous request persists into the next. On the checkout success page this caused isCacheable() to see the previous page's XML (which had no cacheable="false" blocks), returning true and causing DepersonalizePlugin to call clearStorage() and wipe the checkout session before prepareBlockData() could read last_order_id for the print-order button.

readerContext is not covered by opengento's reset.json. Without clearing it, stale scheduledPaths from request 1 cause _overrideElementWorkaround to delete head.js and head.hyva-scripts on request 2+.

Page config — View/Page/Config

Subclasses Magento\Framework\View\Page\Config and implements ResetAfterRequestInterface. Resets elements, pageLayout, includes, and metadata. Fixes the same PHP 8.4 lazy ghost problem as Design.

HTML lang attribute — Plugin/View/Page/ConfigPlugin

Page\Config::_resetState() clears elements = [], which wipes the html.lang attribute set in the constructor. Without html.lang, document.documentElement.lang is "" and Intl.NumberFormat throws (visible as a broken ElasticSuite price slider). The plugin ensures html.lang is always present in getElementAttributes().

Admin ButtonList — Block/Backend/Widget/Context

Widget\Context is a shared singleton holding a ButtonList that is declared shared="false" but is never re-created. Each admin page calls ButtonList::add() on the same instance, so buttons from previous screens accumulate. Our subclass clears _buttons in _resetState().

Admin session lazy start — Plugin/Session/AdminAuthSessionPlugin

With SessionRegistry cleared, startSessions() no longer pre-starts Auth\Session. Without pre-start, _data is empty when Authentication::aroundDispatch() calls isLoggedIn() — the admin appears logged out even with a valid cookie. The plugin calls start() before isLoggedIn() to load _data from Redis.

Admin login race — Plugin/Session/AuthSessionProcessLoginPlugin

If start() in AdminAuthSessionPlugin throws silently (stale cookie, area code unavailable), setUser() writes to orphaned _data. processLogin() then calls regenerateId()session_start()storage->init($_SESSION), overwriting _data from Redis (no user). The backstop checks session_status() before processLogin(), and if the session is not active, re-establishes the reference and re-writes the user before regenerateId() captures $oldSession.

Admin session commit — Plugin/App/SessionCommitPlugin (adminhtml)

On admin login, Auth\Session is regenerated and a 302 redirect is sent. A second worker picks up the GET before the first worker's finally block has written the new session to Redis. closeSessions() in afterLaunch writes all sessions before sendResponse() fires.

REST session commit — Plugin/App/SessionCommitPlugin (webapi_rest)

The REST order placement worker writes last_order_id to CheckoutSession, but session_write_close() only runs in the finally block — after sendResponse(). A frontend worker can start handling the success page GET before this write completes. Same fix: closeSessions() before the response is sent.

REST order session binding — Plugin/Quote/Model/QuoteManagementPlugin

QuoteManagement::placeOrderRun() loads the quote via quoteRepository->getActive(), bypassing checkoutSession->getQuote(). The CheckoutSessionPlugin::aroundGetQuote() therefore never fires during REST order placement, leaving CheckoutSession storage unbound. setLastOrderId() writes to orphaned _data and nothing reaches Redis. beforePlaceOrder() calls checkoutSession->start() before any session writes occur.

REST empty responses — Plugin/App/RestResponseFallbackPlugin

App\Http::handleHttpResult() calls $result->getContent() before sendResponse() is called on the REST response, so exceptions stored via setException() are never rendered — producing 200 OK with an empty body. The plugin detects this and returns the RestResponse directly so AppBootstrap can call sendResponse() on it.

Customer session repair — Plugin/Customer/Model/CustomerSessionPlugin

Two scenarios: (1) Reference breakStorage::_resetState() rebinds $_SESSION[$namespace] = &_data, then session_start() replaces the Redis slot and breaks the reference; _data stays empty. (2) Lazy startupSessionRegistry cleared; session not yet started; _data empty. The plugin calls start() before isLoggedIn() to repair both.

Checkout session lock wait — Plugin/Checkout/Model/CheckoutSessionPlugin

Multiple workers may race to INSERT into quote_address for the same customer. The loser gets MySQL 1205 (lock wait timeout). At that point isLoading=true and _quote=null — a plain retry throws LogicException("Infinite loop"). The plugin catches LockWaitException, calls _resetState() to clear those flags without touching quote_id, and retries.

Checkout config provider guard — Plugin/Checkout/Model/DefaultConfigProviderPlugin

Last-resort recovery: if CustomerSessionPlugin's repair did not fire in time and getConfig() throws NoSuchEntityException from getCustomerId()=null, the plugin repairs CustomerSession, clears the stale guest quote, and retries getConfig() once.

Checkout registration guard — Plugin/Checkout/Block/RegistrationPlugin

If last_order_id is missing from the checkout session when the success page renders, OrderRepository::get(0) throws InputException and breaks the page. The plugin catches this and returns '' so the block is silently hidden.

Success validator session start — Plugin/Checkout/Model/Session/SuccessValidatorPlugin

SuccessValidator::isValid() calls getLastSuccessQuoteId() etc. via __call magic, which reads Storage::getData() directly without triggering a session start. With startSessions() cleared, _data is empty and all three return null, redirecting every post-checkout visit to the cart. The plugin calls start() before isValid().

Preserve order data — Plugin/Checkout/Model/Session/PreserveOrderDataPlugin

Safety net for the Layout._xml root-cause fix. If DepersonalizeChecker incorrectly treats the success page as cacheable and clearStorage() is called, this plugin saves last_real_order_id, last_order_id, and last_order_status before the clear and restores them after. Scoped to checkout_onepage_success only.

Hyva CSP state — ViewModel/HyvaCsp

HyvaCsp caches $memoizedPolicies and $memoizedAreaCode in private properties after the first call. If any request populates $memoizedPolicies without unsafe-eval (e.g. strict-CSP checkout), all subsequent requests serve alpine3-csp.min.js site-wide, breaking every Alpine component. _resetState() nullifies both private parent properties via reflection.

Dependencies

  • opengento/module-application — the FrankenPHP worker scaffolding (required)
  • opengento/magento2-frankenphp-base — the base FrankenPHP worker module (required)
  • Hyva_Theme — the Hyva theme (soft; HyvaCsp preference only applies when Hyva is installed)

Post-install commands

bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 6
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: OSL-3.0
  • 更新时间: 2026-06-13

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固