These notes are for developers who extend, integrate or debug the Magento 2 rental extension. They describe the current release, releaserental2 1.2.205, on Magento 2.4.8 and 2.4.9 with PHP 8.3 or 8.4. For what changed in each version, see the Release Notes.
One rule comes before everything else: never edit the code under vendor/salesigniter/. Composer replaces it on every update. Put your changes in your own module, sequenced after ours, and use plugins, observers and layout XML rather than copies of our classes.
The modules and where they come from #
Everything is installed with Composer from five licence-gated feeds. Each feed has its own host name, and Composer needs a separate http-basic entry for each host you use: the username is the feed name and the password is your licence key. The install pages generate the exact commands for your licence: Regular, then Pro on top of it, and Multi Source Inventory on top of that. To pin a version, see Installing a Specific Version.
| Feed | Repository URL | Serves |
|---|---|---|
| regular | https://rental.rentalbookingsoftware.com/composer/regular | The core extension and its free companions |
| pro | https://rentalpro.rentalbookingsoftware.com/composer/pro | Contracts, maintenance, extensions, deposits and damage waiver, order editing |
| msi | https://rentalmsi.rentalbookingsoftware.com/composer/msi | The Multi Source Inventory integration |
| rfq | https://packagerfq.rentalbookingsoftware.com/composer/rfq | The Amasty RFQ integration |
| vendor | https://rentalvendor.rentalbookingsoftware.com/composer/vendor | The multi-vendor marketplace integration |
| Composer package | Magento module | Feed | What it is | Requires |
|---|---|---|---|---|
salesigniter/releaserental2 | SalesIgniter_Rental | regular | The core: the sirent product type, calendar, pricing, reservations, reports, send and return, reminders, iCal feeds and imports, GraphQL API | releasecommon2, bookingsgrid |
salesigniter/releasecommon2 | SalesIgniter_Common | regular | Shared helpers and the salesigniter:* maintenance commands | 1.2.54 or later on Magento 2.4.9 |
salesigniter/bookingsgrid | SalesIgniter_Bookingsgrid | regular | Customer and admin bookings grids | releaserental2 |
salesigniter/hyvarental | Hyva_SalesIgniterRental | regular | Hyvä companion: add-to-cart button, pricing card and out-of-stock modal. Hyvä stores only. | releaserental2 1.2.197+, hyva-themes/magento2-compat-module-fallback |
salesigniter/releaserentalcontract2 | SalesIgniter_RentalContract | pro | Rental contracts and signatures | releaserental2 1.2.200+ |
salesigniter/releasemaintenance2 | SalesIgniter_Maintenance | pro | Maintenance tickets, which take stock off sale | releaserental2 1.2.198+ |
salesigniter/releaserentalextend2 | SalesIgniter_Rentalextend | pro | Customers extend a rental that is already booked | releaserental2, releasecommon2 |
salesigniter/waiverdamage | SalesIgniter_WaiverDamage | pro | Refundable security deposits and damage waiver | releaserental2 1.2.197+ |
salesigniter/ordereditintegration | SalesIgniter_Orderedit | pro | MageWorx Order Editor integration | releaserental2, releasecommon2 |
salesigniter/releasepurchaserentals2 | SalesIgniter_Purchaserentals | pro | Special pricing for products that are on rent | releaserental2 |
salesigniter/releaserentalinventory2 | SalesIgniter_Rentalinventory | msi | MSI: rental stock per source | 1.2.47 or later on Magento 2.4.9 |
salesigniter/releaserfqintegration2 | SalesIgniter_Rfqintegration | rfq | Amasty Request a Quote integration | releaserental2 |
salesigniter/marketintegration, marketcustom, customvendorpdf | SalesIgniter_Marketintegration and friends | vendor | Vnecoms multi-vendor marketplace integration | releaserental2, releasecommon2 |
The regular feed also carries small bridges for specific third-party modules: partialpay and partialpayshow (Milople Partial Payment), preventstockdeduction (stops MSI source deduction for rentals), cartdates, pickupdropoffdates, releasepdfserial, fixedaddress, mageplazaintegration and preorderfix. Install one only if you run the module it bridges.
composer show "salesigniter/*" lists what an install actually has. The package ships its own design notes in vendor/salesigniter/releaserental2/docs/ (UPGRADING.md, hyva.md, template-plugin-removal.md, calendar-page-cache.md, inventory-rewrite.md, ical-import-architecture.md). They go deeper than this page.
Starting your own module #
Declare a sequence on our module so your plugins, layout and DI load after ours, and require it in your composer.json:
<!-- app/code/Acme/Rental/etc/module.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Acme_Rental">
<sequence>
<module name="SalesIgniter_Rental"/>
</sequence>
</module>
</config>
How the extension is built #
The sirent product type and its attributes #
Rental products are product type sirent (labelled “Reservation” in the admin), with SalesIgniter\Rental\Model\Product\Price as the price model. A sirent product can be a bundle selection or a configurable child. A bundle or configurable becomes a rental through the sirent_rental_type attribute. To ask “is this a rental?”, call SalesIgniter\Rental\Helper\Data::isRentalType($product) rather than comparing type ids, because it handles all three cases.
Every rental product carries four custom options: Start Date:, End Date:, Rental Buyout: and Damage Waiver:. They are created for you on save, including for products created in code through ProductRepositoryInterface::save() (plugin Plugin\Product\EnsureRentalCustomOptions). They are hidden on the storefront, but they are what carries the dates through the quote, the order, invoices and emails. Never delete them.
| Attribute | Holds |
|---|---|
sirent_quantity | How many units the shop owns. StockManagementInterface::getSirentQuantity() reads it (per source on MSI). |
sirent_rental_type | Whether a bundle or configurable uses the rental calendar |
sirent_pricingtype, sirent_price | The pricing type and the rental price grid. Rows live in sirental_price and are saved by Model\Attribute\Backend\RentalPrice. |
sirent_use_times, sirent_hotel_mode | Time of day on the calendar; hotel-style nights |
sirent_min*, sirent_max*, sirent_turnover_before*, sirent_turnover_after*, sirent_padding* | Minimum and maximum period, turnover time and padding |
sirent_excluded_days*, sirent_excluded_dates, sirent_future_limit | Days and dates that cannot be booked, and how far ahead a customer may book |
sirent_enable_buyout, sirent_buyout_price | Buy the item instead of renting it |
sirent_damage_waiver, sirent_deposit | Damage waiver and deposit amounts |
sirent_serial_numbers_use | Track individual units by serial number |
sirent_always_show | Per-product override of the date picker style (range picker or separate fields) |
sirent_allow_overbooking | Per-product overbooking flag. The storefront refusal uses the store setting salesigniter_rental/inventory/allow_overbooking. |
-200 means “use the store setting” #
Helper\Data::USE_CONFIG_DEFAULT is -200. It is stored in a product’s own attribute row to mean “fall back to Stores > Configuration > Sales Igniter > Rental“. It turns up in the period, turnover, padding, excluded-days, fixed-length and damage waiver attributes. So a plain SELECT can make a product look as if it has a damage waiver of minus two hundred. It doesn’t. Don’t “clean up” these rows, and treat an empty value and -200 the same way when you read one. The option list for sirent_fixed_type even includes -200, only so that product imports carrying the sentinel don’t fail.
Reservations: sirental_reservationorders #
Every booking, hold and block is a row in sirental_reservationorders. It is the single source of truth for availability.
| Column | Meaning |
|---|---|
order_id, order_item_id, order_increment_id | The order line. order_id = 0 means there is no order: a manual reservation, a maintenance block or an iCal import. That is why the foreign key to sales_order is declared disabled. |
product_id, qty, qty_cancel | What is reserved and how much, less anything cancelled |
start_date, end_date | The dates the customer chose |
start_date_with_turnover, end_date_with_turnover | The same dates widened by turnover time |
start_date_use_grid, end_date_use_grid, qty_use_grid | What availability actually reads. A row with qty_use_grid = 0 blocks nothing. |
order_type | order, manual, maintenance, rfq or ical |
source_code | The MSI source. NULL counts against every stock. |
qty_shipped, qty_returned, serials_shipped, serials_returned | Send and return. Overdue reminders are decided from these counters. |
history | A log of what touched the row. Read it first when a row looks wrong. |
Rows are written the first time an order is saved (plugin Plugin\Sales\Api\OrderRepositoryInterface::afterSave() on the order resource model), when Reserve inventory is set to “With No Invoice (Recommended)”. With “When Invoiced” they are written when the invoice is paid (Observer\InvoicePay). With a specific status selected, they are written when the order reaches that status. Cron\AsyncInventoryUpdate runs every 15 minutes as a reconciler. It should find nothing. When it does find and reserve an order, it logs a WARNING naming that order. Treat that as a bug to investigate, usually an exception during placement or an order created by code that skips the order resource save.
Availability is derived on read #
Nothing is cached in a table or a product attribute. Every availability question runs the same path:
Model\Inventory\ReservationQueryfetches the live rows for a product and a date window.Model\Inventory\InventoryArrayBuilderturns them into non-overlapping intervals with a sweep line, summing quantity per segment.Model\Inventory\Availabilityanswers “booked quantity”, “fully booked days” and “first date available” from those intervals, memoised for the request.Model\StockManagement(Api\StockManagementInterface) is the public face every caller uses: the calendar, add to cart, checkout validation, the admin reports, GraphQL and the add-ons.
The old salesigniter_rental cache type, its cron job and its console command were removed in 1.2.197. If a deploy still references UpdateRentalCache, clear generated/.
Some quantities never refuse a booking. Unmanaged stock is reported as 99999, and Model\Calendar\Rules::UNMANAGED_QTY treats anything at or above it as unlimited. An admin closure is written as an interval of Stock::OVERBOOK_QTY (9999999) and blocks the dates regardless. Turnover is applied by shifting intervals, not by rebuilding them.
The browser decides what to offer, the server decides what to accept. The rules exist twice, in Model/Calendar/Rules.php and in view/base/web/js/flatpickr/sibookingCalendarRules.js, and shared case tables in Test/fixtures/ are asserted from both sides. If you change one side, change the other too. Otherwise the calendar will offer dates that add to cart then refuses.
Dates are stored as MySQL wall-clock strings #
Since 1.2.196, rental dates are stored as naive store wall-clock datetimes in MySQL format, Y-m-d H:i:s. No time zone conversion happens between the calendar and the database. The time the customer picked is the time stored in sirental_reservationorders, in the quote and order item options, and in the buy request. A whole-day rental runs from 00:00:00 on the first day to 23:59:00 on the last. Model\RentalDates is the value object and reads every legacy shape.
info_buyRequest.calendar_selector = {
"from": "2026-06-23 00:00:00",
"to": "2026-06-25 23:59:00",
"locale": "en_US",
"format": "mysql"
}
For display, go through RentalDateFormatter, which formats the stored value as UTC, which means verbatim. Two mistakes to avoid. Don’t convert rental dates from UTC the way you would Magento’s own created_at, which really is UTC. And don’t zero the time on an end date: that ends every whole-day rental at midnight at the start of its last day, and the calendar frees it a day early.
Stores upgrading from an older version get a one-way, idempotent conversion. A data patch schedules it, Cron\MigrateDates runs it in chunks on the next cron pass, and nothing runs inside setup:upgrade. To run it by hand:
bin/magento salesigniter:rental:migrate-dates --dry-run
bin/magento salesigniter:rental:migrate-dates
# narrower runs
bin/magento salesigniter:rental:migrate-dates --orders-only --limit=500
bin/magento salesigniter:rental:migrate-dates --item-id=1234 --item-id=1235
Rows it cannot parse are reported and left untouched.
Extension points #
Service contracts #
| Interface | Use it to |
|---|---|
Api\StockManagementInterface | getAvailableQuantity($product, $start, $end); checkIntervalValid($product, $dates, $qty); getSirentQuantity($product); getFirstDateAvailable($product); getInventoryArray($product); reserveQuoteOrOrder($order); saveReservation(), cancelReservationQty(), deleteReservationsByOrderId() |
Api\ReservationOrdersRepositoryInterface | Read and save reservation rows through the repository |
Api\SerialNumberDetailsRepositoryInterface | Serial numbers. getList() treats a filter group as OR, not AND, so filter on the collection if you need AND. |
Api\FixedRentalDatesRepositoryInterface, Api\FixedRentalNamesRepositoryInterface | Fixed rental date sets |
Api\ShipmentRepositoryInterface, Api\ReturnsRepositoryInterface | Send and return records |
Api\SendReturnApiProcessorInterface | Ship or return serials. It is also exposed over REST as POST /V1/salesigniter_rental/sendserials and /returnserials, the only REST routes; everything else is GraphQL. It returns a {status_code, status_message} envelope instead of throwing, so check the envelope. |
For an order’s dates, Helper\OrderDates::getDatesForOrder($order) and getRentalDatesForOrderItem($orderId, $productId) read them back from the order.
use SalesIgniter\Rental\Api\StockManagementInterface;
class FreeUnits
{
public function __construct(private StockManagementInterface $stock) {}
public function forDates($product, string $from, string $to): int
{
// $from / $to in Y-m-d H:i:s, store wall-clock
return (int) $this->stock->getAvailableQuantity($product, $from, $to);
}
}
Hooking pricing #
Every rental price comes from Model\Product\PriceCalculations. The storefront quote (salesigniter_rental/ajax/price), the cart and admin order create call calculatePrice($productId, $fromDate, $toDate, $qty, ...), usually by way of Model\Product\Price::getBasePrice(). calculatePrice() calls calculatePriceBreakdown() and returns its price. The GraphQL pricing query calls calculatePriceBreakdown() directly. So an after-plugin on calculatePriceBreakdown() is the one place that changes the price everywhere. It returns price, from_date, to_date, qty, has_special_pricing and chunks. If you change price, change chunks as well, so the GraphQL breakdown still adds up.
<!-- etc/di.xml -->
<type name="SalesIgniter\Rental\Model\Product\PriceCalculations">
<plugin name="acme_rental_weekend_surcharge" type="Acme\Rental\Plugin\WeekendSurcharge"/>
</type>
namespace Acme\Rental\Plugin;
class WeekendSurcharge
{
public function afterCalculatePriceBreakdown($subject, array $result, $productId, $fromDate, $toDate, $qty)
{
// $result['price'] is the rental price for $qty units over the period
return $result;
}
}
A bundle priced per product has no price grid of its own: Plugin\Bundle\Model\Product\Price sums the children’s rental prices. Configurables go through Plugin\Magento\ConfigurableProduct\...\Configurable\Price. The storefront never shows a total of zero or less, which keeps a bundle with nothing selected from printing $0.00.
Hooking availability #
To take stock off sale, write a reservation row rather than intercepting the answer. Every reader, including the calendar, cart validation, reports, GraphQL, MSI and the add-ons, then agrees without any further work. Maintenance tickets and iCal imports both work this way. Use StockManagementInterface::addOrUpdateReservationsOrdersTableRowFromArray() with order_id 0 and an existing order_type such as manual. The inventory report counts an unrecognised type as a customer order.
If you write rows with your own SQL, call Model\InventoryUpdatedObservers::fireObservers($productId) afterwards. It clears the per-request availability memo and purges the product’s sirent_avail_<id> page-cache tag. Without it, cached product pages go on showing the old calendar.
A plugin on checkIntervalValid() or getAvailableQuantity() changes only what the server accepts. The browser calendar builds its own answer from an embedded payload, so it would go on offering the dates. If you must change the rule itself, change both implementations described above.
Events the extension observes #
In 1.2.205 the extension does not dispatch events of its own for bookings. It observes Magento’s, and you can observe the same ones after it, or plug into StockManagement.
| Event | Observer | What it does |
|---|---|---|
checkout_cart_product_add_before, checkout_cart_product_add_after | PreventMixedCheckout*, UpdateGlobalDates, UpdateBundleItemPrice | Mixed-cart rules, global dates, bundle prices |
sales_quote_item_qty_set_after, sales_model_service_quote_submit_before | QuantityValidatorObserver | Refuses dates or quantities that are not available |
catalog_product_type_prepare_full_options | PrepareOptionsObserver | Copies the dates from the buy request into the Start and End Date options |
sales_order_invoice_pay | InvoicePay | Reserves in “When Invoiced” mode |
sales_order_item_cancel, sales_order_creditmemo_save_after | CancelOrderItemObserver, RefundOrderInventoryObserver | Release reserved stock |
sales_order_shipment_save_commit_after | ShipmentSaveCommitted | Send and return bookkeeping |
catalog_product_delete_after_done, sales_order_delete_commit_after | Clean-up observers | Remove reservation rows |
salesrule_rule_condition_combine | RentalConditionRules | Adds rental conditions to cart price rules |
sales_convert_order_item_to_quote_item (admin) | OrderItemToQuoteItem | Keeps dates when an order is edited or reordered |
Plugins and preferences worth knowing #
A preference replaces a class outright. If another module prefers the same class, only one of them wins, and that is the most common kind of conflict. These are ours:
| Replaced | By |
|---|---|
Magento\Sales\Api\OrderItemRepositoryInterface | Model\Order\ItemRepository |
Magento\Bundle\Pricing\Price\BundleSelectionPrice | Plugin\Magento\Bundle\Pricing\Price\BundleSelectionPrice |
Magento\CatalogInventory\Observer\RevertQuoteInventoryObserver and the InventorySales one | Rental-aware versions under Observer\CatalogInventory\ |
Magento\Catalog\Block\Product\ListProduct (frontend) | Block\Product\ListProduct |
The plugins most likely to meet yours:
- Stock checks. Plugins on
StockRegistryInterface,StockStateProviderand, on MSI,IsProductSalableForRequestedQtyConditionChainandGetStockItemConfigurationlet rental products skip Magento’s salable-quantity checks. Rental stock is dates, not a counter. - Order placement.
OrderManagementInterface::place()is wrapped. When a rental order has no shipping, it gets a copy of the billing address andfree_shipping, so an active Free Shipping method must exist. The order resource model’safterSave()writes the reservation rows. - Product save.
ProductRepositoryInterfaceandCatalog\Model\Productplugins create the rental custom options. - Prices on screen.
Framework\Pricing\RenderandPriceBoxplugins render the rental pricing card in place of Magento’s price. - Dates on item lines.
Plugin\Sales\Item\OptionsDateFormatformats the date options on the storefront, in the admin, in emails and on PDFs. - Search. A before-plugin on
Magento\Elasticsearch\...\Aggregation\Intervalnudges a zero price lower bound to1.0e-6. Rental products have a zero catalogue price, and core builds an empty range for a zero bound, which makes OpenSearch return a 400 for the whole category page. Leave it in place: it has no effect once Adobe fixes the core bug. - Every block.
FetchViewModifierwrapsMagento\Framework\View\Element\Template::fetchView(). See the next section.
To find out who wins a class or a plugin chain on a given install, use bin/magento dev:di:info "Magento\Sales\Api\OrderItemRepositoryInterface". When another module breaks the calendar or checkout, first disable it on a staging copy to confirm, then compare its preferences and plugins against the lists above. The fix is usually a sortOrder or a small bridge module of your own.
Themes: Luma, Hyvä and the compatibility switches #
The template plugin, and what replaced it #
Older versions rewrote the finished HTML of blocks: a plugin on Template::fetchView() parsed the HTML and changed it. That renamed buttons, hid options and injected the calendar. Since 1.2.197 most of that work is done the ordinary Magento way, with layout XML, ViewModels, narrowly scoped plugins and two copied core templates:
Magento_Catalog::product/view/addtocart.phtml, used for rental products under thecatalog_product_view_type_sirent,_bundleand_configurablehandles. It renders “Rent” and the Buyout button. If your theme overridesaddtocart.phtml, compare it with ours. For non-rental products the template renders core’s markup unchanged.Magento_Shipping::create/items/renderer/default.phtml, only underadminhtml_order_shipment_new, for the serial pickers.
Rental product pages get the body class sirent-rental-product. The pricing card carries data-role="sirent-pricing-card", and the stylesheet keys on that attribute to hide Magento’s own price displays. A custom card that drops it shows two prices.
Each replaced behaviour has a switch that puts the old HTML-rewriting path back for a customised theme. They are under Stores > Configuration > Sales Igniter > Rental > Theme Compatibility (salesigniter_rental/legacy_template_plugin/*), and all default to No. Turning one on also makes the new code stand down, so the two never both run.
| Switch | Turn it on when |
|---|---|
rent_button | A rental shows “Add to Cart” instead of “Rent”, or the Buyout button is missing, on the product page or in listings |
zero_price | Rental product pages show a $0.00 price |
rental_options | The Start Date, End Date, Rental Buyout or Damage Waiver options are visible, or the calendar is missing |
item_dates | Admin item tables repeat the Start Date and End Date rows, or hide the wrong ones |
serial_inputs | The serial pickers are missing on Sales > Shipments > New Shipment |
admin_order_js | The rental calendar stops working on Create New Order |
The plugin itself stays registered. It still renames the listing buttons (now only on ListProduct and ProductsList), places the admin order calendar, and adds dates to emails and the admin order view. Its per-block cost is gone.
Hyvä #
On a Hyvä store, install salesigniter/hyvarental. It needs hyva-themes/magento2-compat-module-fallback, and supplies the add-to-cart button, the pricing card and the out-of-stock modal in Alpine, with no RequireJS. Its layout files use the hyva_ handle prefix, so they apply to Hyvä themes only, and a Luma store is unaffected. A Luma store should not install it.
Helper\Data::isHyvaTheme()asks Hyvä’s own theme service, so a child ofHyva/defaultis recognised. Force Hyva Theme Mode (salesigniter_rental/theme/is_hyva) forces it on, for headless or heavily customised front ends. There is no way to force it off.- The Previous Date Picker needs jQuery UI and RequireJS, so a Hyvä store always gets the current calendar.
- Layout
before=andafter=only work between siblings, and the block tree differs between Hyvä and Luma. If a block lands in an odd place on your theme, check that its anchor really is a sibling there. Layout never warns about this. - The calendar input has
id="flatpickr", sowindow.flatpickrcan be that element. Test for the library withtypeof window.flatpickr === "function".
The date pickers and their JavaScript #
Calendar Options > Date Picker (salesigniter_rental/calendar_options/picker) has four values: inline (the default), popup, separate and previous. The first three are one flatpickr-based engine, view/base/web/js/flatpickr/sibookingFlatpickr.js, with the rules in sibookingCalendarRules.js and the time slots in the RequireJS module sibookingSlotGrid (window.SibookingTimepicker points at it). previous is the jQuery UI pprdatepicker.js that shipped before 1.2.197, kept so it can be used to rule the new picker out; the admin order screens still use it. The old sitimepicker dropdown is gone, so code that required sibookingTimepicker should require sibookingSlotGrid.
The calendar and the full page cache #
The product page embeds the product’s availability (Model\Calendar\AvailabilityPayload) instead of fetching it on every load, so the page cache must be told when a booking changes it. The calendar block reports the identity sirent_avail_<productId>, and every reservation write purges it through InventoryUpdatedObservers. That works with the built-in cache and with Varnish. Don’t mark the calendar block cacheable="false": one uncacheable block makes the whole page uncacheable.
A related trap: Advanced > Record out of stock rentals (salesigniter_rental/advanced/record_outofstock) adds an uncacheable block to every page. With it on, the full page cache stores nothing, site-wide. Check it before you measure cache behaviour.
GraphQL API #
The rental API is GraphQL, on Magento’s own /graphql endpoint, with nothing to enable. It is documented in its own section:
- GraphQL API Overview & Authentication
- Availability & Calendar Queries and Pricing Queries
- Reservations & Bookings and Retrieving Orders by Rental Date
- Serial Numbers and Sending & Returning Rentals
- Booking from a Headless Storefront and Maintenance Tickets
When you extend the schema, these points matter:
- The schema is in each module’s
etc/schema.graphqls. Resolvers live inModel/Resolver/, and shared services inModel/GraphQl/(Authorization,ProductLocator,DateInput,Pagination,PriceFormatterand others). Reuse them rather than writing parallel ones. - Back-office fields are authorised with the existing ACL: an admin or integration bearer token that can reach
/rest/V1can reach them. Storefront fields are not gated. etc/graphql/di.xmlregisters a type resolver forsirent. Without it, the standardproductsquery returnsnullfor every rental product and reports “Concrete type for ProductInterface not implemented”.- The schema stitcher reads comments. Magento finds types in
.graphqlsfiles with a regular expression over the raw text, and it doesn’t skip#comments. A comment such as “This type adds that” defines a type calledadds, the real type disappears, and the whole endpoint fails withSyntax Error: Unexpected Name "that", naming no file. Never writetype,interface,union,enum,inputorscalarfollowed by a word in a comment. Document fields in"""docstrings or@doc(description:)instead.php vendor/salesigniter/releaserental2/Test/E2e/schema-lint.phpruns the same regular expression and names the offending line. - After adding a type resolver or other GraphQL DI,
cache:flushmay not be enough. Cleargenerated/metadataas well.
Cron jobs and CLI commands #
Magento cron must run, ideally every minute. None of these jobs is needed to reserve stock at checkout, but reminders, iCal imports, the Utilization report and the date migration all wait on it.
| Job | Group | Runs | Does |
|---|---|---|---|
salesigniter_rental_send_reminders | sirent_reminders | every 5 min | Queues and sends reminder emails. It is safe to run late or twice, because reminders are de-duplicated in sirental_reminders. |
async_inventory_update | sirent_reminders | every 15 min | The reservation reconciler (see above) |
salesigniter_rental_migrate_dates | sirent_reminders | every 5 min | Runs the date migration while it is pending, otherwise nothing |
update_utilization | sirent_utilization | every minute | Builds Utilization reports that have been requested |
salesigniter_rental_sync_ical_imports | sirent_ical | every 10 min | Syncs subscribed external calendars, salesigniter_rental/ical/import_batch_size at a time |
| Command | Module | Does |
|---|---|---|
salesigniter:rental:migrate-dates | releaserental2 | Date storage migration (--dry-run, --orders-only, --quotes-only, --item-id, --limit) |
salesigniter:Remove:Attributes, salesigniter:Restore:Attributes | releasecommon2 | Detach the rental attribute source models before disabling the extension, and put them back after re-enabling |
salesigniter:Uninstall | releasecommon2 | Deletes the rental attributes, tables and reservation history. Read Uninstall or Disable first. |
Tests #
The tests ship with the package under vendor/salesigniter/releaserental2/Test/. Run them from the Magento root. Judge a PHPUnit run by its OK or FAILURES line, not by the exit status: a missing Allure config file adds a runner warning that sets the exit status to 1 on its own.
# unit - no database
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist vendor/salesigniter/releaserental2/Test/Unit
# integration against your DEVELOPMENT database - creates and deletes its own products and orders
vendor/bin/phpunit -c vendor/salesigniter/releaserental2/Test/Integration/phpunit.xml
# calendar rules in node - one file at a time; "node --test Test/Js/" fails before running anything
node --test vendor/salesigniter/releaserental2/Test/Js/rules.test.js
Never point the integration suite at a production database. It writes real products, orders and reservations, and cleans up after itself only when it finishes. Some of its tests use one hand-maintained product, whose id is SI_TEST_PRODUCT_ID in its phpunit.xml. Point that at a date-only rental product of your own.
The schema tests in Test/Integration/Schema need a clean install, so they run under Magento’s own integration framework instead:
rm -rf generated/metadata # not optional, see below
cd dev/tests/integration
php -d memory_limit=-1 ../../../vendor/bin/phpunit -c phpunit.xml.dist \
../../../vendor/salesigniter/releaserental2/Test/Integration/Schema
Browser tests (Playwright) and a GraphQL end-to-end check are in Test/E2e/; Test/E2e/README.md explains the setup. graphql-smoke.sh proves the schema stitches, every resolver can be constructed and the ACL refuses what it should.
The traps, each of which has cost somebody a day:
generated/metadataoverrides everydi.xml. When compiled DI exists, Magento reads nothing else, and the integration framework uses the dev install’s compiled DI. A DI change then appears to do nothing, with no message. Deletegenerated/metadatabefore an integration run and after any DI change.- Stale generated code looks like a missing class or a bad argument. After switching branches or pulling a constructor change, errors like
Class ... not foundor “Argument #12 must be of type …, array given” usually meangenerated/is stale. Runrm -rf generated/code/* generated/metadata/*, thensetup:di:compile. - Adminhtml plugins don’t fire in the harness. It boots the frontend area, so a test of an
etc/adminhtml/di.xmlplugin has to apply the plugin by hand. - Place test orders on a store view, not store 0. MSI resolves stock from the website, and the admin website has none.
- A test that runs DDL needs
@magentoDbIsolation disabledon the class docblock. Otherwise the firstDROP TABLEwaits forever on the framework’s own open transaction. Kill the leftover MariaDB threads before the next run.
Debugging #
- Why is a date blocked? Query the rows:
SELECT reservationorder_id, order_id, order_type, qty_use_grid, start_date_use_grid, end_date_use_grid, source_code, history FROM sirental_reservationorders WHERE product_id = 123 AND end_date_use_grid > NOW() ORDER BY start_date_use_grid;. A row withorder_id = 0is a manual, maintenance or iCal block.commentsstartingical:names the import. - What does the calendar think? The product page embeds the availability payload. A popup picker or an admin screen calls
salesigniter_rental/ajax/booked. Prices come fromsalesigniter_rental/ajax/price. Watch the browser’s network tab and compare the answer with the rows. - The calendar is right after a refresh but wrong from cache: something wrote reservation rows without calling
InventoryUpdatedObservers::fireObservers(). - Orders reserved late: search
var/log/system.logfor “reconciliation cron reserved inventory”. The ERROR logged at placement just before it gives the cause. - A new plugin or preference has no effect:
cache:flush, thensetup:di:compilein production mode.cache:clean configalone has been seen not to be enough. - “Please upgrade your database” after deploying:
setup:upgradedidn’t finish. Run it again and read its output. Don’t put helper classes inSetup/Patch/Data/: Magento registers every PHP file there as a patch. - Luma looks broken after an upgrade but the logs are clean: stale static files. See the next section.
Upgrading to Magento 2.4.9 #
2.4.9 runs on PHP 8.3 or 8.4. None of the steps below announces itself when it goes wrong:
- Raise the rental modules in the same Composer run as Magento. 2.4.9 brings Symfony Console 7, where
Command::execute()must returnint. Olderreleasecommon2andreleaserentalinventory2commands don’t, and Magento loads every command, so everybin/magentocall dies, includingsetup:upgrade. Requirereleasecommon21.2.54 or later and, on MSI,releaserentalinventory21.2.47 or later. Both work on 2.4.8 too. Third-party modules with the same problem (Xtento’s exporters, for example) must be updated or disabled. - Re-apply security patches. Composer replaces the patched vendor files. Check Adobe’s bulletins for fixes shipped as separate hotfixes for 2.4.9, such as APSB26-146, and apply them before leaving maintenance mode.
- Wipe the static files instead of redeploying over them.
setup:static-content:deploy -fskips files that already exist, so the new markup gets the old CSS. On Luma the top navigation collapses to zero height while HTTP, the logs and the file count all look fine. A partial wipe can leave RequireJS unable to resolve module names. Always do the full wipe below. - Reindex after
setup:upgrade: it invalidates indexers without rebuilding them. - Redis: 2.4.9 moved the cache to Symfony Cache.
backendinenv.phpmust be the literal stringredis. A unix-socket connection is built into an invalid DSN, and Magento quietly falls back to the file cache. Ifvar/cachefills up while Redis stays empty, use a TCP host and port or patchSymfonyAdapterProvider.
bin/magento maintenance:enable
composer require magento/product-community-edition:2.4.9 --no-update
composer require "salesigniter/releasecommon2:>=1.2.54" --no-update
composer require "salesigniter/releaserentalinventory2:>=1.2.47" --no-update # MSI only
composer update
bin/magento setup:upgrade
bin/magento setup:di:compile # production mode
rm -rf pub/static/frontend pub/static/adminhtml pub/static/deployed_version.txt
rm -rf var/view_preprocessed/* var/cache/* var/page_cache/*
bin/magento setup:static-content:deploy -f en_US # add your locales
bin/magento indexer:reindex
bin/magento cache:flush
bin/magento maintenance:disable
Then check the storefront in a real browser. A 200 response and a clean log proved nothing on the installs where the static-file problem appeared. PHP 8.4 raises deprecation notices, not errors, for older contract, maintenance and extension add-on versions; update them anyway.
Conventions in this codebase #
- Schema is declarative.
etc/db_schema.xmlplusdb_schema_whitelist.json; the oldInstallSchemaandUpgradeSchemaare gone. Name composite indexes with an explicitreferenceId, or everysetup:upgradechurns them. Setup/Patch/Data/holds patch classes only, each named after its file. Shared code lives one directory up.- Dates are MySQL wall-clock strings everywhere (see above). Format them only at the edge.
- Rules exist twice, in PHP and in JavaScript, held together by the shared fixtures in
Test/fixtures/. - Reservation writes announce themselves through
InventoryUpdatedObservers. A unit test,ReservationWritePathsTest, fails when a file writes rows without doing so. - A refusal is recorded, never silent. Reminders that are not sent get a row with a reason. The iCal import never frees dates because a fetch failed.
- GraphQL fields are documented in docstrings, never in
#comments. - Versions:
composer.jsonversionandetc/module.xmlsetup_versionalways match, and tags are the bare version, for example1.2.205.
