Development Notes
These notes describe the current development approach and technical decisions in the plugin.
Code Style
- New PHP files belong below
src. - New PHP files need a file docblock,
declare(strict_types=1);, and the WordPress direct-access guard after namespace/use statements. - PHP 8.2 features may be used.
- Service dependencies are injected through constructor property promotion.
- Services use private properties and no trivial getters/setters.
- Value objects and DTOs should preferably be
readonly final classwith public promoted properties. - When a class is already
readonly, do not mark promoted constructor properties asreadonlyagain. - Use short array syntax
[]in new code. - User-facing strings in code stay in English and use the
storeaccountanttext domain. - For normal UI strings, pass the natural English text to WordPress translation
functions, for example
__( 'Export Format', 'storeaccountant' ). Do not replace regular labels with opaque source keys such asexport_format_label. - Synthetic translation keys are allowed only for values that are selected by
runtime IDs, for example
export_adapter_plus a registered adapter ID,exporter_plus a renderer ID, orstorage_adapter_plus a storage adapter ID. Built-in dynamic keys must be kept discoverable through static translation calls such as the catalogue entries insrc/I18n.php. - Keep
languages/storeaccountant-en_US.poandlanguages/storeaccountant-en_US.mocomplete alongside the German files. English source strings still belong in PHP, but the English catalogue gives dynamic keys readable labels and gives translators a complete source-aligned file to work from. - Namespaced PHP files should import global functions with
use function ...;. Keep using this convention for PHP built-ins and WordPress/WooCommerce global functions instead of relying on implicit namespace fallback or leading-slash calls. use functiononly resolves the function name; it does not load optional WordPress helper files. Queue, cron, REST, AJAX, and CLI code must explicitly load optional admin includes before calling helpers that live there, such aswp-admin/includes/file.phpforwp_tempnam().- Prefer WordPress helper APIs over direct PHP alternatives when a suitable
WordPress API exists. This applies especially to filesystem access,
sanitization, escaping, redirects, uploads, HTTP requests, dates, and database
access. For example, use the WordPress Filesystem API for regular file
contents and deletion instead of direct
file_put_contents()orunlink(). - Treat
phpcs:ignoreas a last resort. Before adding one, verify whether the warning can be resolved through a WordPress API, input validation, sanitization, escaping, or a small code structure change. If an ignore is still necessary, document why the warning cannot be fixed directly and ask the project owner for confirmation before committing the ignore.
WooCommerce HPOS Compatibility
All future order-related features must stay compatible with WooCommerce High-Performance Order Storage (HPOS). StoreAccountant declares compatibility with WooCommerce custom order tables in the plugin bootstrap, so new code must not introduce direct WordPress post or postmeta access for WooCommerce orders.
Use WooCommerce APIs for order data:
- Query orders with
wc_get_orders()orWC_Order_Query. - Load a single order with
wc_get_order(). - Read order fields through
WC_Ordergetters. - Read custom order metadata with
$order->get_meta(). - Write custom order metadata with
$order->update_meta_data()or relatedWC_Ordermeta methods, followed by$order->save()when persistence is required.
Before declaring a new order feature done, audit it against WooCommerce's HPOS
search terms such as wpdb, get_post_meta, update_post_meta, get_posts,
WP_Query, and shop_order. Matches are allowed for StoreAccountant's own custom
post types, export records, and configuration records, but not for WooCommerce
order records.
Container and Services
The container is configured in src/ContainerBuilder.php. Not every class is a
service:
- Services with behavior and dependencies belong in the container.
- Value objects such as
ExportPerioddo not belong in the container. - Classes that register WordPress hooks implement
HookRegistrarInterfaceand are listed inContainerBuilder::HOOK_SERVICES.
Admin UI
StoreAccountant registers its own top-level Accounting admin menu. The visible
sidebar item is Exports, which links to the generated export records and keeps
the existing Exports / Export Configurations tab navigation intact.
StoreAccountant intentionally uses native WordPress custom post type list tables for exports and export configurations. This keeps search, pagination, trash, and bulk deletion available.
Custom actions are injected above the native list through AccountingHeaderBar.
The custom post types themselves are hidden from the default sidebar placement
and are surfaced through the dedicated accounting menu entries.
The export overview exposes export creation through a server-side form. The
first selector option opens the Quick Export page; saved export configuration
options submit directly and create an export from the selected configuration.
Plugin settings currently include Storage Locations, Transports,
Integrations, Security, and Permissions tabs.
The export overview polls visible saved export run rows through
admin-ajax.php while a run can still change soon. Polling is scoped to
storeacct_export records, not export configurations. Rows with queued or
processing status are always pollable. Rows with scheduled status are only
pollable when the export run stores _storeaccountant_scheduled_for and that
time is within the configured scheduled polling window. Completed and failed
exports are terminal and are removed from polling.
The polling endpoint returns row DTOs with translated status and progress labels,
frontend download URLs, and pollability decisions. PHP remains responsible for
download capabilities; assets/js/export-list-polling.js only updates marked
DOM cells. When a polled export becomes completed and a generated file is still
available in storage, the list row shows the Download button without requiring a
page refresh.
Export download URLs contain only the public download token. Frontend downloads
must verify the export password against the stored password hash and stream the
file returned by StorageAdapterInterface::get_file(). Download code must stay
adapter-agnostic and must not infer local uploads paths from stored references.
Export Filters
Exports and export configurations store source filters as JSON. Date ranges are
not generic export metadata. Use filter providers for export-type specific
selection, for example order date and order status filters for the orders
export adapter.
Runtime filters register through storeaccountant_export_filter. Admin field
providers register through storeaccountant_export_filter_field_provider. Period
resolvers used by date-like filters register through
storeaccountant_export_filter_period_provider.
Server-side Validation
Forms must not rely on client-side validation.
Current state:
- Export configurations require a title server-side.
- Exports started from a saved configuration require a non-empty, unique title submitted from the export overview form.
- Filter values are validated server-side by filter field providers.
- Order status filters are loaded from
wc_get_order_statuses()and validated server-side against the available WooCommerce statuses. - Export format must exist in the export adapter registry.
- Storage adapter must be registered and enabled.
The free month/year period resolver used by the order date filter accepts only:
all_timecurrent_monthlast_month- month values
1to12 - years inside the calculated range from the last ten years up to the current year
Future month values in the current year are rejected.
The all_time selection covers the beginning of the selectable ten-year window
through the current shop time.
Date filter periods are selected in the shop timezone from wp_timezone() and
resolved as UTC datetimes. During export generation those UTC datetimes are
converted to timestamps on WC_Order_Query, so WooCommerce compares the
selected date field against GMT storage columns with second precision.
Export and Configuration Relations
An export can have a nullable relation to an export configuration:
- Quick export: do not store a configuration relation.
- Export by configuration: store the configuration ID on the export.
The export overview renders that relation in the Configuration column.
Existing configurations are linked to their edit form. Quick exports without a
configuration relation are displayed as Quick Export; relations to deleted
configurations are retained and displayed as deleted.
Quick exports and saved export configurations store filter selections. When a configuration is used to start an export, its filter selection is copied to the generated export record and dynamic period selections are snapshotted.
When an export configuration is deleted, existing exports must be kept. The stored relation on the export is retained so the export overview can indicate that the related configuration was deleted.
Export configurations are edited through the custom StoreAccountant configuration form so title, filters, export format, storage adapter, and additional provider settings stay in sync. Native list edit links are redirected to that custom form.
When an export is deleted, its export configuration must not be deleted.
Manual export starts must create a storeacct_export record and dispatch queue
work through Symfony's MessageBusInterface. Admin form handlers should not
call StoreAccountant\Export\Exporter::export() directly. Export-specific queue
messages and handlers belong below StoreAccountant\Export\Queue, while
reusable queue infrastructure and transports belong below
StoreAccountant\Queue. Queue messages should be plain serializable DTOs, and
handlers should be invokable callables registered in Symfony's
HandlersLocatorInterface.
Batch-capable export adapters implement
StoreAccountant\Export\Contract\BatchExportAdapterInterface. Batch handlers
load deterministic source slices, normalize them, and store temporary fragments
through StoreAccountant\Export\Queue\BatchExportStore. Finalization streams
those fragments into the configured renderer and persists the resulting artifact
through the selected storage adapter. Do not add custom database tables for
batch state.
Batch size is configured per export configuration and copied to each export
record when the export is created. Quick exports store their submitted batch
size directly on the export record. The queue start handler must use the saved
export batch size rather than a global setting so retries remain stable.
User-submitted batch sizes must be numeric and at least 10; the default is
50.
Scheduled exports are not currently offered in the admin UI and are not
configured on export configuration records. When scheduled export workflows
return, they should use a focused scheduling model. A scheduled run should still
create a normal storeacct_export record, snapshot the configuration state
needed for execution, and dispatch the same export queue message used by manual
exports.
Storage and Paths
Storage adapters expose a small StoreAccountant API: ID, persist(),
delete_file(), directory operations, visibility operations, file_exists(),
file metadata getters, get_file(), and readiness checks. Export adapters generate temporary files; storage adapters
decide how those files are persisted, checked, and later opened for downloads.
They do not need to expose Flysystem methods or derive folder/file names from
export titles. ExportStoragePathGenerator builds the storage reference before
the adapter is called and formats display paths. persist() receives a
StorageFileConfiguration value object with the generated source path, storage
reference, target file name, and optional internal archive path.
The local adapter receives LocalStorageConfiguration for backend-specific
root, archive, and display paths instead of resolving upload paths itself.
Local zip storage references point to the archive itself, for example
exports/accounting-may-2026.zip. The optional internal archive path is used
only while writing the generated CSV/XML into the archive.
Downloads for local exports return the generated zip archive, not the inner CSV
or XML file stored inside the archive.
Local exports use the local storage adapter and are stored in a Flysystem zip
archive below. The local adapter implements Flysystem internally and decorates
League's ZipArchive adapter. The archive file name is part of the generated
storage reference:
Saved export titles must be unique across export custom post type records. The admin create flow validates this before creating the export record so local zip generated file names do not need an ID suffix.
Absolute server paths must not be printed in admin notices. Saved export meta
stores paths inside the selected storage adapter, such as
exports/accounting-may-2026.zip;
user-facing local display paths start with wp-content/....
When the local storage root is created, LocalStorageAdapter must create
.htaccess and index.html. On plugin deactivation, the directory may only be
removed when it does not contain foreign files, subdirectories, or the generated
zip archive.
wp-env Setup
The local WordPress environment is configured in .wp-env.json in the plugin
repository root. The plugin is mapped into the container at:
Run wp-env commands from the plugin repository root. The development
environment uses the cli environment for WP-CLI, PHP, and Composer commands.
Start the WordPress environment:
Stop the WordPress environment:
Run a WP-CLI command:
Activate the StoreAccountant plugin:
Run Composer for this plugin:
Require a Composer package for this plugin:
wp-env run cli composer require vendor/package --working-dir=/var/www/html/wp-content/plugins/storeaccountant
Verification
After PHP changes, run at least php -l through the wp-env CLI environment
from the plugin repository root:
When adding or changing translatable strings:
wp-env run cli wp i18n make-pot /var/www/html/wp-content/plugins/storeaccountant /var/www/html/wp-content/plugins/storeaccountant/languages/storeaccountant.pot --exclude=vendor,languages --skip-js
wp-env run cli wp i18n update-po /var/www/html/wp-content/plugins/storeaccountant/languages/storeaccountant.pot /var/www/html/wp-content/plugins/storeaccountant/languages/storeaccountant-de_DE.po
wp-env run cli wp i18n make-mo /var/www/html/wp-content/plugins/storeaccountant/languages /var/www/html/wp-content/plugins/storeaccountant/languages