Services
A service is a class with reusable business logic that does not belong to a model, a controller or a view: counting records for the sidebar badge, calculating prices, converting currencies, parsing an API query, writing history. Services keep the Loader, controllers, boards and crons thin and make sure that the same number is calculated in the same way everywhere.
In your app, create services in the Services/ folder:
Services/
├── Counter.php
├── PriceCalculator.php
└── CurrencyConverter.php
What a service looks like
A service is a plain class that extends Hubleto\Erp\Core. The base class gives it the shortcuts used everywhere else: getModel(), getService(), appManager(), config(), env(), authProvider(), translate(), ...
apps/PriceLists/Services/Counter.php (excerpt)
<?php
namespace Hubleto\App\Community\PriceLists\Services;
use Hubleto\App\Community\PriceLists\Models\History;
use Hubleto\App\Community\PriceLists\Models\PriceList;
/**
* Counters used in the sidebar badge, alerts, dashboards and crons.
*/
class Counter extends \Hubleto\Erp\Core
// ...
public function expiringPriceLists(?int $days = null): array
{
/** @var Settings $settings */
$settings = $this->getService(Settings::class);
$days = $days ?? $settings->getInt('expiryWarningDays');
/** @var PriceList $mPriceList */
$mPriceList = $this->getModel(PriceList::class);
$t = $mPriceList->table;
return $mPriceList->record
->where($t . '.status', PriceList::STATUS_ACTIVE)
->whereNotNull($t . '.valid_to')
->where($t . '.valid_to', '>=', date('Y-m-d'))
->where($t . '.valid_to', '<=', date('Y-m-d', strtotime('+' . $days . ' days')))
->orderBy($t . '.valid_to', 'asc')
->get()
->toArray()
;
}
NOTE A service needs a constructor without parameters, because the dependency injection container creates it with
new.
Creating and using a service
Services are created with getService() and are singletons: the first call creates the object and every other call during the same request returns the same instance. Always add a @var comment so that your IDE knows its methods.
Using a service in the Loader
public function getSidebarBadgeNumber(): int
{
/** @var Services\Counter $counter */
$counter = $this->getService(Services\Counter::class);
return $counter->expiringPriceLists() + $counter->draftPriceLists();
}
The same Counter is used for the badge, the alerts, the sidebar buttons and the board, so all of them show the same numbers:
apps/PriceLists/Loader.php (excerpt)
public function getSidebarBadgeNumber(): int
{
/** @var Counter $counter */
$counter = $this->getService(Counter::class);
return $counter->draftPriceLists() + count($counter->expiringPriceLists());
}
Services in controllers, crons and other services
A service can use other services and models. Controllers and crons call the service instead of repeating the query:
apps/PriceLists/Crons/WarnAboutExpiry.php (excerpt)
public function run(): void
{
/** @var Counter $counter */
$counter = $this->getService(Counter::class);
/** @var Sender $sender */
$sender = $this->getService(Sender::class);
foreach ($counter->expiringPriceLists() as $priceList) {
$idOwner = (int) ($priceList['id_owner'] ?? 0);
if ($idOwner <= 0) continue;
$sender->send(
900,
['price-lists', 'expiry'],
PriceList::class,
(int) $priceList['id'],
$idOwner,
$this->translate('Price list is about to expire'),
$priceList['code'] . ' ' . $priceList['name'] . ' - ' . $this->translate('valid to') . ' ' . $priceList['valid_to'],
'price-lists/' . $priceList['id']
);
}
}
A service that needs data from other apps does not hard-code them. ItemAdapterRegistry collects the adapters that other apps provide through an extendible:
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;
}
Services of the core
Many core features are services. You get them with the same getService() or with a shortcut such as $this->router():
| Shortcut / class | Purpose |
|---|---|
$this->router() |
URL parameters, route registration |
$this->appManager() |
Registry of the apps (getApp(), getEnabledApps()) |
$this->config() |
Configuration saved in the database |
$this->authProvider() |
The signed-in user (getUserId(), getUserType()) |
$this->permissionsManager() |
Permission checks |
$this->eventManager() |
Events and listeners |
$this->cronManager() |
Registration of crons |
$this->db() |
Database connection |
$this->translate() |
Translation of a string |
Hubleto\App\Community\Documents\Generator |
Generation of PDF documents |
Hubleto\App\Community\Notifications\Sender |
Internal notifications |
Hubleto\App\Community\Settings\Services\Currency |
Currency conversion and formatting |
Hubleto\App\Community\Invoices\Services\PriceCalculator |
Price, VAT and discount calculation |
Examples of services.
Replacing a service (service providers)
The container can map one class to another. Hubleto uses it to replace the framework implementations with the ERP ones (e.g. the framework Services\Env with Hubleto\Erp\Env). Because the container is static, such a mapping must be registered before the service is first used, for example in the constructor of a custom Loader class of your project:
src/Loader.php (excerpt)
DependencyInjection::setServiceProviders([
\Hubleto\Framework\Services\Renderer::class => Renderer::class,
\Hubleto\Framework\Services\Env::class => Env::class,
\Hubleto\Framework\Services\Locale::class => Locale::class,
\Hubleto\Framework\Controllers\NotFound::class => Controllers\NotFound::class,
\Hubleto\Framework\Controllers\Desktop::class => \Hubleto\App\Community\Desktop\Controllers\Desktop::class,
]);
Use it sparingly. Replace a service only when you must change a behavior of the core; otherwise create your own service.
Where not to put logic
| Do not put it in… | Put it in… |
|---|---|
The Loader class |
A service (Services/Counter.php); the Loader only calls it |
| A controller | A service, or a model method when it works with one record |
| A Twig view | The controller (prepareView()) or a service |
| A React component | An API controller + service, and call it with request.get() |
NOTE Older community apps keep helper classes in the root of the app (
Deals/Counter.php,Invoices/Counter.php). The newer ones (PriceLists,Products,Invoices\Services\PriceCalculator,Settings\Services\Currency) useServices/. Create new services inServices/.
