Extendibles
An extendible is a small class in the Extendibles/ folder of an app that contributes a list of items to a feature of another app. The app that owns the feature collects the items from all enabled apps. The collecting app does not know the contributors in advance, so you can extend the menu, the product types, the tools or the context help of Hubleto just by adding a file to your app.
How it works
- The app that owns the feature calls
collectExtendibles('Name')on itsLoader. - For each enabled app the container creates the class
<app namespace>\Extendibles\Name(if it exists), sets its$appproperty and callsgetItems(). - The returned arrays are merged into one array, which the collecting app uses.
framework: src/App.php (excerpt)
public function collectExtendibles(string $extendibleName): array
{
$items = [];
foreach ($this->appManager()->getEnabledApps() as $app) {
try {
$extendible = $this->getService($app->namespace . '\\Extendibles\\' . $extendibleName);
$extendible->app = $app;
if ($extendible instanceof Extendible) {
$items = array_merge($items, $extendible->getItems());
}
} catch (\Throwable $e) {
// do nothing
}
}
return $items;
}
Apps that do not have the class are skipped silently, so an extendible is always optional.
NOTE The items are merged with
array_merge(). Items with string keys override each other, but items with integer keys are renumbered ([10 => 'a']becomes[0 => 'a']). Return a plain list or use string keys when the keys matter.
Extendibles collected by the community apps
| Extendible name | Collected by | getItems() returns |
Example |
|---|---|---|---|
AppMenu |
Desktop |
menu entries (app, url, title, icon) of the app |
PriceLists\Extendibles\AppMenu |
Tools |
Tools |
tools shown on the Tools page | Settings\Extendibles\Tools |
ContextHelp |
Help |
context help: URL fragment → documentation path per language | Customers\Extendibles\ContextHelp |
MailTemplateVariables |
Mail |
variables that can be used in e-mail templates | Deals\Extendibles\MailTemplateVariables |
ProductTypes |
Products |
types of products and where they come from | Deals\Extendibles\ProductTypes |
PriceListItemAdapters |
PriceLists |
adapters that map item models to the fields of the price calculator | PriceLists\Extendibles\PriceListItemAdapters |
PriceCalculatorConsumers |
PriceLists |
models that use the price calculator | PriceLists\Extendibles\PriceCalculatorConsumers |
Extendibles of the community apps.
Contributing to an extendible
Create the class in Extendibles/ of your app, extend Hubleto\Framework\Extendible and return the items. The property $this->app is the Loader of your app, use it to translate texts.
Adding the app to the desktop menu
apps/PriceLists/Extendibles/AppMenu.php
<?php
namespace Hubleto\App\Community\PriceLists\Extendibles;
class AppMenu extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
[
'app' => $this->app,
'url' => 'price-lists',
'title' => $this->app->translate('Price lists'),
'icon' => 'fas fa-tags',
],
[
'app' => $this->app,
'url' => 'price-lists/items',
'title' => $this->app->translate('Prices'),
'icon' => 'fas fa-list',
],
[
'app' => $this->app,
'url' => 'price-lists/vat-rates',
'title' => $this->app->translate('VAT rates'),
'icon' => 'fas fa-receipt',
],
[
'app' => $this->app,
'url' => 'price-lists/simulator',
'title' => $this->app->translate('Price simulator'),
'icon' => 'fas fa-flask',
],
];
}
}
Adding variables to e-mail templates
apps/Deals/Extendibles/MailTemplateVariables.php
<?php
namespace Hubleto\App\Community\Deals\Extendibles;
class MailTemplateVariables extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
'deal.identifier',
'deal.price_excl_vat',
];
}
}
Adding a tool
apps/Settings/Extendibles/Tools.php
<?php
namespace Hubleto\App\Community\Settings\Extendibles;
class Tools extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
[
'title' => $this->app->translate('Log viewer'),
'icon' => 'fas fa-screwdriver-wrench',
'url' => 'settings/log-viewer',
]
];
}
}
Adding a link to the context help
apps/Customers/Extendibles/ContextHelp.php
<?php
namespace Hubleto\App\Community\Customers\Extendibles;
class ContextHelp extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
'customers' => [
'en' => 'en/apps/community/customers',
],
];
}
}
In your custom app the files have the same shape, only the namespace changes:
<?php
namespace Hubleto\App\Custom\CarRental\Extendibles;
class AppMenu extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
[
'app' => $this->app,
'url' => 'car-rental',
'title' => $this->app->translate('Car rental'),
'icon' => 'fas fa-car',
],
];
}
}
Creating your own extendible
Create an extendible when your app needs other apps to provide data to it. PriceLists lets any app describe how its document items map to a price list:
1. Collect the items from all apps
apps/PriceLists/Services/ItemAdapterRegistry.php (excerpt)
public function getAdapters(): array
{
if ($this->adapters === null) {
/** @var PriceListApp|null $app */
$app = $this->appManager()->getApp('Hubleto\\App\\Community\\PriceLists');
$this->adapters = $app ? $app->collectExtendibles('PriceListItemAdapters') : [];
}
return $this->adapters;
}
2. Let other apps contribute
apps/PriceLists/Extendibles/PriceListItemAdapters.php
<?php
namespace Hubleto\App\Community\PriceLists\Extendibles;
use Hubleto\App\Community\Cashdesk\Models\Receipt;
use Hubleto\App\Community\Cashdesk\Models\ReceiptItem;
use Hubleto\App\Community\Deals\Models\Deal;
use Hubleto\App\Community\Deals\Models\Item as DealItem;
use Hubleto\App\Community\Invoices\Models\Invoice;
use Hubleto\App\Community\Invoices\Models\Item as InvoiceItem;
use Hubleto\App\Community\Orders\Models\Item as OrderItem;
use Hubleto\App\Community\Orders\Models\Order;
/**
* Declares document-item models that PriceList can price. Other apps can provide their own
* Extendibles\PriceListItemAdapters class with the same shape (see community-apps-patches.md).
*
* Adapter keys:
* productField, quantityField, priceField, discountField, vatField,
* outputMap (canonical calculator key => column; leave empty when the item model recalculates its own prices),
* parent => model, fkField, customerField, supplierField, currencyField, dateField,
* typeField, purchaseValue, shopField, cashRegisterField
*/
class PriceListItemAdapters extends \Hubleto\Framework\Extendible
{
public function getItems(): array
{
return [
OrderItem::class => [
'productField' => 'id_product', 'quantityField' => 'amount',
'priceField' => 'unit_price', 'discountField' => 'discount', 'vatField' => 'vat',
'outputMap' => [],
'parent' => [
'model' => Order::class, 'fkField' => 'id_order',
'customerField' => 'id_customer', 'supplierField' => 'id_supplier', 'currencyField' => 'id_currency',
'dateField' => 'date_order', 'typeField' => 'purchase_sales', 'purchaseValue' => Order::PURCHASE_ORDER,
],
],
DealItem::class => [
'productField' => 'id_product', 'quantityField' => 'amount',
'priceField' => 'unit_price', 'discountField' => 'discount', 'vatField' => 'vat',
'outputMap' => [],
'parent' => [
'model' => Deal::class, 'fkField' => 'id_deal',
'customerField' => 'id_customer', 'currencyField' => 'id_currency',
],
],
InvoiceItem::class => [
'productField' => 'id_product', 'quantityField' => 'amount',
'priceField' => 'unit_price', 'discountField' => 'discount', 'vatField' => 'vat',
'outputMap' => [],
'parent' => [
'model' => Invoice::class, 'fkField' => 'id_invoice',
'customerField' => 'id_customer', 'currencyField' => 'id_currency', 'dateField' => 'date_issue',
],
],
ReceiptItem::class => [
'productField' => 'id_product', 'quantityField' => 'quantity',
'priceField' => 'unit_price_excl_vat', 'discountField' => null, 'vatField' => 'vat_percent',
'outputMap' => [
'unit_price_excl_vat' => 'unit_price_excl_vat', 'unit_vat' => 'unit_vat', 'unit_price_incl_vat' => 'unit_price_incl_vat',
'price_excl_vat' => 'total_price_excl_vat', 'price_vat' => 'total_vat', 'price_incl_vat' => 'total_price_incl_vat',
],
'parent' => [
'model' => Receipt::class, 'fkField' => 'id_receipt', 'cashRegisterField' => 'id_cash_register', 'dateField' => 'created',
],
],
];
}
}
Guidelines
- Give the extendible a name that says what it contains (
MailTemplateVariables, notVariables). - Describe the shape of an item in the docblock of the collecting class, as
ItemAdapterRegistrydoes. - Collect once and cache the result in a property. The loop over all enabled apps is not free.
- Do not require the extendible. The collecting code must work when no app contributes.
