www.hubleto.eu Reddit User Guide Stack Overflow LinkedIn Report an issue

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) use Services/. Create new services in Services/.