Helpers — traits and helper services
OrangeHRM has a lot of helpers scattered across the core plugin. The naming convention is consistent: most are use-able traits, and each trait typically wraps one helper service. Before writing a util function, check this catalog — the answer is usually "already exists."
This skill is a reference catalog. Each entry shows what to use, what method(s) it exposes, and the common case. For deeper documentation on specific helpers, see the linked skill.
How the trait pattern works (recap)
The codebase consistently uses traits as the DI access layer:
class MyClass
{
use DateTimeHelperTrait; // ← gives $this->getDateTimeHelper()
use TextHelperTrait; // ← gives $this->getTextHelper()
use UserRoleManagerTrait; // ← gives $this->getUserRoleManager()
public function doSomething(): void
{
$now = $this->getDateTimeHelper()->getNow();
$clean = $this->getTextHelper()->strip(...);
}
}
Traits typically delegate to the DI container or instantiate the helper lazily. Same pattern in services, decorators, subscribers, commands, endpoints, validators — everywhere. See services skill for the full pattern.
Core helper services + their traits
These are the "everyday" helpers — formatters, normalizers, cross-cutting utilities. Each has a service class in orangehrmCorePlugin/Service/ and a matching trait in orangehrmCorePlugin/Traits/Service/.
| Trait |
Service |
Exposes |
Use for |
DateTimeHelperTrait |
DateTimeHelperService |
getNow(), formatDate(), formatDateTime(), getDateTimeHelper() (returns the service), TIMEZONE_UTC constant |
Anything date/time. Always use this rather than new DateTime() directly — it respects the user's timezone. |
TextHelperTrait |
TextHelperService |
strStartsWith(), strEndsWith(), strContains(), truncate(), stripTags(), more string ops |
Polyfills for older PHP str_* methods and shared text munging. |
NumberHelperTrait |
NumberHelperService |
Number formatting, rounding |
Currency-like display, fixed-decimal output. |
NormalizerServiceTrait |
NormalizerService |
normalize($entityOrCollection, $class) |
Convert a Doctrine entity to a Model's array output without going through EndpointResult. Used by services that need to emit Model-shaped data outside the REST flow. |
ConfigServiceTrait |
ConfigService |
The full config getter/setter catalog |
See config skill. |
MenuServiceTrait |
MenuService |
Side panel + top menu rendering |
Internal — used by the Twig layout. Rarely called from feature code. |
ReportGeneratorServiceTrait |
ReportGeneratorService |
Report rendering |
Internal to the reports system. |
Plugin-specific service traits (representative)
Every plugin has its own Traits/Service/<Name>ServiceTrait.php for each of its services. The pattern is universal — see services skill. Most-frequently used from outside their owning plugin:
| Trait |
Use for |
UserServiceTrait (Admin) |
Current user lookups, user management |
EmployeeServiceTrait (Pim) |
Employee operations |
LeaveServiceTrait (Leave) |
Leave allocation, approval flows |
CompanyStructureServiceTrait (Admin) |
Subunit tree operations |
EmailServiceTrait (Core, via Mail) |
Sending emails — see mail skill |
When pulling in a cross-plugin service, you use that plugin's trait. The dependency between plugins is fine; the trait keeps consumers from instantiating the service directly.
Framework-level traits (no separate service)
Most of these wrap framework-provided objects (event dispatcher, logger, cache, validator) rather than OHRM-specific services.
| Trait |
Exposes |
Use for |
ServiceContainerTrait |
getContainer(): Container |
Raw DI container access — rare in feature code; mostly used by other traits internally. Avoid unless you specifically need it. |
EventDispatcherTrait |
getEventDispatcher() |
Dispatch events. See events skill. |
LoggerTrait |
getLogger() |
Monolog logger writing to src/log/orangehrm.log. Use for error, warning, info, debug log messages. |
CacheTrait |
getCache($namespace) |
Symfony Cache adapter access. Per-namespace; common namespace is orangehrm for app-level caching. |
ValidatorTrait |
validate($values, ParamRuleCollection) |
Programmatic invocation of the validator — same call the REST framework makes. Use for validating data outside the REST flow (e.g. in a CLI command, in a CSV importer). See rest-validation. |
ClassHelperTrait |
getClassHelper(): ClassHelper |
Reflection + class name resolution. Used internally by the home-page enabler lookup, the menu configurator resolver, etc. |
ETagHelperTrait |
generateETag($data), setETag($response, $etag) |
ETag generation for HTTP caching headers — used by file controllers and the OXD logo endpoint. |
Authentication / authorization traits
| Trait |
Exposes |
Use for |
AuthUserTrait |
getAuthUser(): AuthUser |
Current authenticated user — session-bound. getUserId(), getEmpNumber(), isAuthenticated(), attribute storage. |
UserRoleManagerTrait |
getUserRoleManager() |
The whole RBAC system — see authorization skill. getAccessibleEntityIds(), getApiPermissions(), getScreenPermissions(), etc. |
Doctrine / ORM traits
Already covered in detail in daos skill:
| Trait |
Exposes |
Use for |
EntityManagerTrait |
getEntityManager() |
Bare EM access. |
EntityManagerHelperTrait |
(extends EntityManagerTrait) getRepository(), persist(), remove(), createQueryBuilder(), getPaginator(), getQueryBuilderWrapper(), fetchOne(), getReference(), beginTransaction()/commitTransaction()/rollBackTransaction() |
The DAO surface — but also usable in services, decorators, validator rules, listeners. |
Controller / module traits
| Trait |
Exposes |
Use for |
ControllerTrait |
forward($controllerSpec) for sub-controller dispatch |
Used by exception subscribers to render error pages via another controller (e.g. ForbiddenController from ScreenAuthorizationSubscriber). Rare in feature code. |
ModuleScreenHelperTrait |
getCurrentModuleAndScreen() |
Resolves the active module + screen from the URL — used by ScreenAuthorizationSubscriber. Rare in feature code. |
Controller/VueComponentPermissionTrait |
Helpers for AbstractVueController to filter component props by permission |
Inside preRender() to decide what data to pass to the Vue component. |
Encryption / security traits
See the security-primitives skill for the full treatment. Quick references:
| Trait |
Exposes |
Use for |
EncryptionHelperTrait (under Utility/) |
encryptionEnabled(), getCryptographer() |
Used inside EntityListener classes to encrypt/decrypt sensitive columns. |
Plain Helper/ classes (not traits)
orangehrmCorePlugin/Helper/ has a few non-trait helpers — instantiate with new or accessed via a service.
| Class |
Use for |
ClassHelper |
classExists($className, $namespace = ''), getClass(). Reflection-style class resolution with fallback namespace. Used by HomePageService and menu configurators. |
VueControllerHelper |
Builds the context passed to the Vue Twig template (baseUrl, user, permissions, etc.). Internal to AbstractVueController. |
ModuleScreenHelper |
Static utility for module/screen URL parsing. Used by ModuleScreenHelperTrait. |
LocalizedDateFormatter |
Locale-aware date formatting. Used inside DateTimeHelperService. |
Utility/ plain classes
orangehrmCorePlugin/Utility/ has security and infrastructure utilities. Mostly covered in other skills:
| Class |
Skill |
Cryptographer |
security-primitives |
KeyHandler |
security-primitives |
PasswordHash |
security-primitives |
Mailer, MailMessage, MailTransport |
mail |
Sanitizer |
HTML sanitization for user-generated content. Used in Buzz posts. |
Base64Url |
URL-safe base64 encoding (for tokens, query params). |
"Is there a helper for…" cheat sheet
| If you need… |
use this |
| Current date/time |
DateTimeHelperTrait → getDateTimeHelper()->getNow() |
| Format a date for the user |
DateTimeHelperTrait → getDateTimeHelper()->formatDate($date) |
| Check if a string starts with X |
TextHelperTrait → getTextHelper()->strStartsWith($str, $prefix) |
| Truncate a string |
TextHelperTrait → getTextHelper()->truncate($str, $max) |
| Strip HTML tags |
TextHelperTrait → getTextHelper()->stripTags() |
| Format a number with currency |
NumberHelperTrait → getNumberHelper()->... |
| Log an error |
LoggerTrait → getLogger()->error('msg', $context) |
| Log info during a long operation |
LoggerTrait → getLogger()->info('progress: 50%') |
| Get the current user |
AuthUserTrait → getAuthUser()->getEmpNumber() / getUserId() |
| Check if user has a permission |
UserRoleManagerTrait → getUserRoleManager()->getApiPermissions(...) (see authorization) |
| Get accessible entity IDs (e.g. employees this user can see) |
UserRoleManagerTrait → getUserRoleManager()->getAccessibleEntityIds(Employee::class) |
| Read a config value |
ConfigServiceTrait → getConfigService()->getX() (see config) |
| Dispatch an event |
EventDispatcherTrait → getEventDispatcher()->dispatch(...) (see events) |
| Run the validator on arbitrary data |
ValidatorTrait → validate($values, $rules) (see rest-validation) |
| Cache something |
CacheTrait → getCache('orangehrm')->get(key, callback) |
| Generate an ETag for a response |
ETagHelperTrait → generateETag($data), setETag($response, $etag) |
| Encrypt a sensitive field on save |
(in an EntityListener) EncryptionHelperTrait → getCryptographer()->encrypt(...) (see security-primitives) |
| Create a database query |
EntityManagerHelperTrait → createQueryBuilder(Entity::class, 'e') (see daos) |
| Persist an entity |
EntityManagerHelperTrait → persist($entity) (see daos) |
| Start a transaction |
EntityManagerHelperTrait → beginTransaction() (see daos) |
If your need isn't in the table: search src/plugins/orangehrmCorePlugin/Traits/ and Traits/Service/ for matching trait names. The naming is predictable — XHelperTrait, XServiceTrait. If nothing exists for the operation you need, the right answer is to add a helper service + trait, not to inline the logic in a feature.
When to add a new helper
A new helper is justified when:
- The same logic appears in two places already (3rd time = definitely extract). One-off logic stays inline.
- The logic is non-trivial — formatting, parsing, computation worth more than 5 lines.
- It needs DI — accessing config, the EM, the dispatcher, etc. Goes in a service; trait wraps it.
- It's pure — no I/O, no state. Goes in a static utility class or a plain helper.
Adding a new helper service + trait
Mirror an existing example like TextHelperService / TextHelperTrait:
// src/plugins/orangehrmCorePlugin/Service/SlugHelperService.php
namespace OrangeHRM\Core\Service;
class SlugHelperService
{
public function slugify(string $input): string
{
return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($input)));
}
}
// src/plugins/orangehrmCorePlugin/Traits/Service/SlugHelperTrait.php
namespace OrangeHRM\Core\Traits\Service;
use OrangeHRM\Core\Service\SlugHelperService;
trait SlugHelperTrait
{
protected ?SlugHelperService $slugHelper = null;
public function getSlugHelper(): SlugHelperService
{
if (!$this->slugHelper instanceof SlugHelperService) {
$this->slugHelper = new SlugHelperService();
}
return $this->slugHelper;
}
}
For pure helpers like this, no container registration is needed — the trait instantiates on demand. For helpers that need DI (config, EM, dispatcher), follow the services skill's full registration pattern.
Recipes
Recipe 1 — Service that uses several common traits
namespace OrangeHRM\X\Service;
use OrangeHRM\Core\Traits\EventDispatcherTrait;
use OrangeHRM\Core\Traits\LoggerTrait;
use OrangeHRM\Core\Traits\Auth\AuthUserTrait;
use OrangeHRM\Core\Traits\Service\ConfigServiceTrait;
use OrangeHRM\Core\Traits\Service\DateTimeHelperTrait;
use OrangeHRM\Core\Traits\Service\TextHelperTrait;
use OrangeHRM\Core\Traits\UserRoleManagerTrait;
class WidgetService
{
use EventDispatcherTrait;
use LoggerTrait;
use AuthUserTrait;
use ConfigServiceTrait;
use DateTimeHelperTrait;
use TextHelperTrait;
use UserRoleManagerTrait;
public function buildSlug(string $name): string
{
$clean = $this->getTextHelper()->stripTags($name);
return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $clean));
}
public function logOperation(string $operation): void
{
$this->getLogger()->info(sprintf(
'[%s] User %d: %s',
$this->getDateTimeHelper()->getNow()->format('Y-m-d H:i:s'),
$this->getAuthUser()->getUserId(),
$operation,
));
}
public function getAccessibleWidgets(): array
{
if (!$this->getConfigService()->isWidgetFancyModeEnabled()) {
return [];
}
return $this->getUserRoleManager()->getAccessibleEntityIds(Employee::class);
}
}
The bigger the service, the more traits it composes — that's fine. The trait list is the service's dependency declaration. Reading from the top quickly tells you what it depends on.
Recipe 2 — Add a new helper
Goal: a MoneyHelperService for formatting currency strings consistently.
namespace OrangeHRM\Core\Service;
class MoneyHelperService
{
public function format(float $amount, string $currencyCode = 'USD'): string
{
return $currencyCode . ' ' . number_format($amount, 2, '.', ',');
}
}
namespace OrangeHRM\Core\Traits\Service;
use OrangeHRM\Core\Service\MoneyHelperService;
trait MoneyHelperTrait
{
protected ?MoneyHelperService $moneyHelper = null;
public function getMoneyHelper(): MoneyHelperService
{
return $this->moneyHelper ??= new MoneyHelperService();
}
}
Now any class can use MoneyHelperTrait and call $this->getMoneyHelper()->format($amount, $currency). No further registration needed — pure helper, no DI dependencies.
Recipe 3 — Pick the right trait by responsibility
The mental model: think about what you're doing, then pick the matching trait.
- "Format a date for the UI" → date formatting →
DateTimeHelperTrait
- "Log that this happened" → logging →
LoggerTrait
- "Send an event so other plugins can react" → events →
EventDispatcherTrait
- "Check if the user is allowed to do this" → permissions →
UserRoleManagerTrait + see authorization
- "Encrypt this before saving" → encryption (entity listener) →
EncryptionHelperTrait + see security-primitives
- "Read a feature flag" → config →
ConfigServiceTrait + see config
- "Run a DQL query" → ORM →
EntityManagerHelperTrait + see daos
If you find yourself doing two of these at once, you use two traits. The trait list grows with the class's responsibilities. That's normal — services routinely use 4-8 traits.
Checklists
Before writing a new helper
Compose helpers into a class
Add a new helper service + trait
Things that bite
- Don't bypass the trait and instantiate helpers directly with
new ConfigService(). Some traits cache the service per-class; bypassing means an extra instantiation per call. More importantly, it breaks the convention — other devs look for use ConfigServiceTrait and don't find it.
- Don't put business logic in a helper — helpers are utilities (formatters, normalizers, lookups). Business logic that involves the domain belongs in a Service (see
services skill).
AuthUserTrait only works inside a request lifecycle. In console commands or migrations, the session isn't bound — getAuthUser()->getUserId() returns null. Either pass a user ID as a command argument or work without one.
getEntityManager() is fine in any context that's run after Framework is bootstrapped (which is everywhere except very early install bootstrap). Use it freely.
use-ing two traits with the same method name is a fatal error in PHP. Rare here because OHRM trait method names are distinctive, but if you ever see Trait method getX has not been applied, you've got a name collision — rename one of the methods.
- Helpers are stateless. Don't store request-specific data on a helper service — different callers might share the same instance. If you need request state, use the service itself, not a helper.
ServiceContainerTrait is internal plumbing — don't use it directly from feature code. Use the higher-level traits that wrap container access.
1---2name: helpers3description: Catalog of OrangeHRM's framework-wide helper traits and helper services — the trait composition pattern (services, decorators, subscribers, commands, endpoints all `use` these), the `core helper services` (`DateTimeHelperService`, `NumberHelperService`, `TextHelperService`, `NormalizerService`, `MenuService`), the framework-level traits (`ServiceContainerTrait`, `EventDispatcherTrait`, `LoggerTrait`, `CacheTrait`, `ValidatorTrait`, `ClassHelperTrait`, `ETagHelperTrait`, `ModuleScreenHelperTrait`, `ControllerTrait`, `UserRoleManagerTrait`, `AuthUserTrait`, `EntityManagerHelperTrait`/`EntityManagerTrait`), and the `Helper/` plain-class helpers (`ClassHelper`, `VueControllerHelper`, `ModuleScreenHelper`, `LocalizedDateFormatter`). Use whenever the user is about to write a util function for something common (date formatting, string operations, number formatting, current-user lookup, cache access, etc.) — to find out if there's already a helper to `use` instead. Companion to `services` (the canonical trait consume4---56# Helpers — traits and helper services78OrangeHRM has **a lot of helpers** scattered across the core plugin. The naming convention is consistent: most are `use`-able traits, and each trait typically wraps one helper service. **Before writing a util function**, check this catalog — the answer is usually "already exists."910This skill is a reference catalog. Each entry shows what to `use`, what method(s) it exposes, and the common case. For deeper documentation on specific helpers, see the linked skill.1112## How the trait pattern works (recap)1314The codebase consistently uses traits as the DI access layer:1516```php17class MyClass18{19 use DateTimeHelperTrait; // ← gives $this->getDateTimeHelper()20 use TextHelperTrait; // ← gives $this->getTextHelper()21 use UserRoleManagerTrait; // ← gives $this->getUserRoleManager()2223 public function doSomething(): void24 {25 $now = $this->getDateTimeHelper()->getNow();26 $clean = $this->getTextHelper()->strip(...);27 }28}29```3031Traits typically delegate to the DI container or instantiate the helper lazily. Same pattern in services, decorators, subscribers, commands, endpoints, validators — everywhere. See `services` skill for the full pattern.3233## Core helper services + their traits3435These are the "everyday" helpers — formatters, normalizers, cross-cutting utilities. Each has a service class in `orangehrmCorePlugin/Service/` and a matching trait in `orangehrmCorePlugin/Traits/Service/`.3637| Trait | Service | Exposes | Use for |38|---|---|---|---|39| `DateTimeHelperTrait` | `DateTimeHelperService` | `getNow()`, `formatDate()`, `formatDateTime()`, `getDateTimeHelper()` (returns the service), `TIMEZONE_UTC` constant | Anything date/time. **Always** use this rather than `new DateTime()` directly — it respects the user's timezone. |40| `TextHelperTrait` | `TextHelperService` | `strStartsWith()`, `strEndsWith()`, `strContains()`, `truncate()`, `stripTags()`, more string ops | Polyfills for older PHP `str_*` methods and shared text munging. |41| `NumberHelperTrait` | `NumberHelperService` | Number formatting, rounding | Currency-like display, fixed-decimal output. |42| `NormalizerServiceTrait` | `NormalizerService` | `normalize($entityOrCollection, $class)` | Convert a Doctrine entity to a Model's array output without going through `EndpointResult`. Used by services that need to emit Model-shaped data outside the REST flow. |43| `ConfigServiceTrait` | `ConfigService` | The full config getter/setter catalog | See `config` skill. |44| `MenuServiceTrait` | `MenuService` | Side panel + top menu rendering | Internal — used by the Twig layout. Rarely called from feature code. |45| `ReportGeneratorServiceTrait` | `ReportGeneratorService` | Report rendering | Internal to the reports system. |4647### Plugin-specific service traits (representative)4849Every plugin has its own `Traits/Service/<Name>ServiceTrait.php` for each of its services. The pattern is universal — see `services` skill. Most-frequently `use`d from outside their owning plugin:5051| Trait | Use for |52|---|---|53| `UserServiceTrait` (Admin) | Current user lookups, user management |54| `EmployeeServiceTrait` (Pim) | Employee operations |55| `LeaveServiceTrait` (Leave) | Leave allocation, approval flows |56| `CompanyStructureServiceTrait` (Admin) | Subunit tree operations |57| `EmailServiceTrait` (Core, via Mail) | Sending emails — see `mail` skill |5859When pulling in a cross-plugin service, you `use` *that plugin's* trait. The dependency between plugins is fine; the trait keeps consumers from instantiating the service directly.6061## Framework-level traits (no separate service)6263Most of these wrap framework-provided objects (event dispatcher, logger, cache, validator) rather than OHRM-specific services.6465| Trait | Exposes | Use for |66|---|---|---|67| `ServiceContainerTrait` | `getContainer(): Container` | Raw DI container access — rare in feature code; mostly used by other traits internally. Avoid unless you specifically need it. |68| `EventDispatcherTrait` | `getEventDispatcher()` | Dispatch events. See `events` skill. |69| `LoggerTrait` | `getLogger()` | Monolog logger writing to `src/log/orangehrm.log`. Use for `error`, `warning`, `info`, `debug` log messages. |70| `CacheTrait` | `getCache($namespace)` | Symfony Cache adapter access. Per-namespace; common namespace is `orangehrm` for app-level caching. |71| `ValidatorTrait` | `validate($values, ParamRuleCollection)` | Programmatic invocation of the validator — same call the REST framework makes. Use for validating data outside the REST flow (e.g. in a CLI command, in a CSV importer). See `rest-validation`. |72| `ClassHelperTrait` | `getClassHelper(): ClassHelper` | Reflection + class name resolution. Used internally by the home-page enabler lookup, the menu configurator resolver, etc. |73| `ETagHelperTrait` | `generateETag($data)`, `setETag($response, $etag)` | ETag generation for HTTP caching headers — used by file controllers and the OXD logo endpoint. |7475## Authentication / authorization traits7677| Trait | Exposes | Use for |78|---|---|---|79| `AuthUserTrait` | `getAuthUser(): AuthUser` | Current authenticated user — session-bound. `getUserId()`, `getEmpNumber()`, `isAuthenticated()`, attribute storage. |80| `UserRoleManagerTrait` | `getUserRoleManager()` | The whole RBAC system — see `authorization` skill. `getAccessibleEntityIds()`, `getApiPermissions()`, `getScreenPermissions()`, etc. |8182## Doctrine / ORM traits8384Already covered in detail in `daos` skill:8586| Trait | Exposes | Use for |87|---|---|---|88| `EntityManagerTrait` | `getEntityManager()` | Bare EM access. |89| `EntityManagerHelperTrait` | (extends EntityManagerTrait) `getRepository()`, `persist()`, `remove()`, `createQueryBuilder()`, `getPaginator()`, `getQueryBuilderWrapper()`, `fetchOne()`, `getReference()`, `beginTransaction()`/`commitTransaction()`/`rollBackTransaction()` | The DAO surface — but also usable in services, decorators, validator rules, listeners. |9091## Controller / module traits9293| Trait | Exposes | Use for |94|---|---|---|95| `ControllerTrait` | `forward($controllerSpec)` for sub-controller dispatch | Used by exception subscribers to render error pages via another controller (e.g. `ForbiddenController` from `ScreenAuthorizationSubscriber`). Rare in feature code. |96| `ModuleScreenHelperTrait` | `getCurrentModuleAndScreen()` | Resolves the active module + screen from the URL — used by `ScreenAuthorizationSubscriber`. Rare in feature code. |97| `Controller/VueComponentPermissionTrait` | Helpers for `AbstractVueController` to filter component props by permission | Inside `preRender()` to decide what data to pass to the Vue component. |9899## Encryption / security traits100101See the `security-primitives` skill for the full treatment. Quick references:102103| Trait | Exposes | Use for |104|---|---|---|105| `EncryptionHelperTrait` (under `Utility/`) | `encryptionEnabled()`, `getCryptographer()` | Used inside `EntityListener` classes to encrypt/decrypt sensitive columns. |106107## Plain `Helper/` classes (not traits)108109`orangehrmCorePlugin/Helper/` has a few non-trait helpers — instantiate with `new` or accessed via a service.110111| Class | Use for |112|---|---|113| `ClassHelper` | `classExists($className, $namespace = '')`, `getClass()`. Reflection-style class resolution with fallback namespace. Used by `HomePageService` and menu configurators. |114| `VueControllerHelper` | Builds the context passed to the Vue Twig template (baseUrl, user, permissions, etc.). Internal to `AbstractVueController`. |115| `ModuleScreenHelper` | Static utility for module/screen URL parsing. Used by `ModuleScreenHelperTrait`. |116| `LocalizedDateFormatter` | Locale-aware date formatting. Used inside `DateTimeHelperService`. |117118## `Utility/` plain classes119120`orangehrmCorePlugin/Utility/` has security and infrastructure utilities. Mostly covered in other skills:121122| Class | Skill |123|---|---|124| `Cryptographer` | `security-primitives` |125| `KeyHandler` | `security-primitives` |126| `PasswordHash` | `security-primitives` |127| `Mailer`, `MailMessage`, `MailTransport` | `mail` |128| `Sanitizer` | HTML sanitization for user-generated content. Used in Buzz posts. |129| `Base64Url` | URL-safe base64 encoding (for tokens, query params). |130131## "Is there a helper for…" cheat sheet132133| If you need… | `use` this |134|---|---|135| Current date/time | `DateTimeHelperTrait` → `getDateTimeHelper()->getNow()` |136| Format a date for the user | `DateTimeHelperTrait` → `getDateTimeHelper()->formatDate($date)` |137| Check if a string starts with X | `TextHelperTrait` → `getTextHelper()->strStartsWith($str, $prefix)` |138| Truncate a string | `TextHelperTrait` → `getTextHelper()->truncate($str, $max)` |139| Strip HTML tags | `TextHelperTrait` → `getTextHelper()->stripTags()` |140| Format a number with currency | `NumberHelperTrait` → `getNumberHelper()->...` |141| Log an error | `LoggerTrait` → `getLogger()->error('msg', $context)` |142| Log info during a long operation | `LoggerTrait` → `getLogger()->info('progress: 50%')` |143| Get the current user | `AuthUserTrait` → `getAuthUser()->getEmpNumber()` / `getUserId()` |144| Check if user has a permission | `UserRoleManagerTrait` → `getUserRoleManager()->getApiPermissions(...)` (see `authorization`) |145| Get accessible entity IDs (e.g. employees this user can see) | `UserRoleManagerTrait` → `getUserRoleManager()->getAccessibleEntityIds(Employee::class)` |146| Read a config value | `ConfigServiceTrait` → `getConfigService()->getX()` (see `config`) |147| Dispatch an event | `EventDispatcherTrait` → `getEventDispatcher()->dispatch(...)` (see `events`) |148| Run the validator on arbitrary data | `ValidatorTrait` → `validate($values, $rules)` (see `rest-validation`) |149| Cache something | `CacheTrait` → `getCache('orangehrm')->get(key, callback)` |150| Generate an ETag for a response | `ETagHelperTrait` → `generateETag($data)`, `setETag($response, $etag)` |151| Encrypt a sensitive field on save | (in an EntityListener) `EncryptionHelperTrait` → `getCryptographer()->encrypt(...)` (see `security-primitives`) |152| Create a database query | `EntityManagerHelperTrait` → `createQueryBuilder(Entity::class, 'e')` (see `daos`) |153| Persist an entity | `EntityManagerHelperTrait` → `persist($entity)` (see `daos`) |154| Start a transaction | `EntityManagerHelperTrait` → `beginTransaction()` (see `daos`) |155156If your need isn't in the table: search `src/plugins/orangehrmCorePlugin/Traits/` and `Traits/Service/` for matching trait names. The naming is predictable — `XHelperTrait`, `XServiceTrait`. **If nothing exists for the operation you need, the right answer is to add a helper service + trait, not to inline the logic in a feature.**157158## When to add a new helper159160A new helper is justified when:1611621. **The same logic appears in two places already** (3rd time = definitely extract). One-off logic stays inline.1632. **The logic is non-trivial** — formatting, parsing, computation worth more than 5 lines.1643. **It needs DI** — accessing config, the EM, the dispatcher, etc. Goes in a service; trait wraps it.1654. **It's pure** — no I/O, no state. Goes in a static utility class or a plain helper.166167### Adding a new helper service + trait168169Mirror an existing example like `TextHelperService` / `TextHelperTrait`:170171```php172// src/plugins/orangehrmCorePlugin/Service/SlugHelperService.php173namespace OrangeHRM\Core\Service;174175class SlugHelperService176{177 public function slugify(string $input): string178 {179 return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($input)));180 }181}182```183184```php185// src/plugins/orangehrmCorePlugin/Traits/Service/SlugHelperTrait.php186namespace OrangeHRM\Core\Traits\Service;187188use OrangeHRM\Core\Service\SlugHelperService;189190trait SlugHelperTrait191{192 protected ?SlugHelperService $slugHelper = null;193194 public function getSlugHelper(): SlugHelperService195 {196 if (!$this->slugHelper instanceof SlugHelperService) {197 $this->slugHelper = new SlugHelperService();198 }199 return $this->slugHelper;200 }201}202```203204For pure helpers like this, no container registration is needed — the trait instantiates on demand. For helpers that need DI (config, EM, dispatcher), follow the `services` skill's full registration pattern.205206---207208# Recipes209210## Recipe 1 — Service that uses several common traits211212```php213namespace OrangeHRM\X\Service;214215use OrangeHRM\Core\Traits\EventDispatcherTrait;216use OrangeHRM\Core\Traits\LoggerTrait;217use OrangeHRM\Core\Traits\Auth\AuthUserTrait;218use OrangeHRM\Core\Traits\Service\ConfigServiceTrait;219use OrangeHRM\Core\Traits\Service\DateTimeHelperTrait;220use OrangeHRM\Core\Traits\Service\TextHelperTrait;221use OrangeHRM\Core\Traits\UserRoleManagerTrait;222223class WidgetService224{225 use EventDispatcherTrait;226 use LoggerTrait;227 use AuthUserTrait;228 use ConfigServiceTrait;229 use DateTimeHelperTrait;230 use TextHelperTrait;231 use UserRoleManagerTrait;232233 public function buildSlug(string $name): string234 {235 $clean = $this->getTextHelper()->stripTags($name);236 return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $clean));237 }238239 public function logOperation(string $operation): void240 {241 $this->getLogger()->info(sprintf(242 '[%s] User %d: %s',243 $this->getDateTimeHelper()->getNow()->format('Y-m-d H:i:s'),244 $this->getAuthUser()->getUserId(),245 $operation,246 ));247 }248249 public function getAccessibleWidgets(): array250 {251 if (!$this->getConfigService()->isWidgetFancyModeEnabled()) {252 return [];253 }254 return $this->getUserRoleManager()->getAccessibleEntityIds(Employee::class);255 }256}257```258259The bigger the service, the more traits it composes — that's fine. The trait list is the service's dependency declaration. Reading from the top quickly tells you what it depends on.260261## Recipe 2 — Add a new helper262263Goal: a `MoneyHelperService` for formatting currency strings consistently.264265```php266namespace OrangeHRM\Core\Service;267268class MoneyHelperService269{270 public function format(float $amount, string $currencyCode = 'USD'): string271 {272 return $currencyCode . ' ' . number_format($amount, 2, '.', ',');273 }274}275```276277```php278namespace OrangeHRM\Core\Traits\Service;279280use OrangeHRM\Core\Service\MoneyHelperService;281282trait MoneyHelperTrait283{284 protected ?MoneyHelperService $moneyHelper = null;285286 public function getMoneyHelper(): MoneyHelperService287 {288 return $this->moneyHelper ??= new MoneyHelperService();289 }290}291```292293Now any class can `use MoneyHelperTrait` and call `$this->getMoneyHelper()->format($amount, $currency)`. No further registration needed — pure helper, no DI dependencies.294295## Recipe 3 — Pick the right trait by responsibility296297The mental model: **think about what you're doing, then pick the matching trait.**298299- "Format a date for the UI" → date formatting → `DateTimeHelperTrait`300- "Log that this happened" → logging → `LoggerTrait`301- "Send an event so other plugins can react" → events → `EventDispatcherTrait`302- "Check if the user is allowed to do this" → permissions → `UserRoleManagerTrait` + see `authorization`303- "Encrypt this before saving" → encryption (entity listener) → `EncryptionHelperTrait` + see `security-primitives`304- "Read a feature flag" → config → `ConfigServiceTrait` + see `config`305- "Run a DQL query" → ORM → `EntityManagerHelperTrait` + see `daos`306307If you find yourself doing two of these at once, you `use` two traits. The trait list grows with the class's responsibilities. That's normal — services routinely `use` 4-8 traits.308309---310311# Checklists312313## Before writing a new helper314315- [ ] Search `Traits/` and `Traits/Service/` in `orangehrmCorePlugin` for matching trait names316- [ ] Search the "Is there a helper for…" cheat sheet above317- [ ] Check other plugins' `Traits/Service/` — sometimes a plugin owns a helper that's broadly useful318- [ ] Only if nothing exists, add a new helper service + trait — mirror existing pattern from `TextHelperService` or similar319320## Compose helpers into a class321322- [ ] `use <Helper>Trait;` at the top of the class323- [ ] Call `$this->get<Helper>()` to access the helper service324- [ ] Need multiple? List them all — no upper limit, services routinely use 4-8 traits325326## Add a new helper service + trait327328- [ ] Service class in `src/plugins/orangehrm{X}Plugin/Service/<Name>HelperService.php`329- [ ] Matching trait in `Traits/Service/<Name>HelperTrait.php` with `?<Name>HelperService $field` and lazy `get<Name>Helper()` method330- [ ] If the helper needs DI (config, EM, dispatcher) — register in plugin's `PluginConfiguration::initialize()` and have the trait fetch from the container instead of `new`-ing331- [ ] If pure (no DI), the lazy `new` is fine — no container registration needed332- [ ] Add a row to your project's "Is there a helper for…" table so future devs find it333334## Things that bite335336- **Don't bypass the trait** and instantiate helpers directly with `new ConfigService()`. Some traits cache the service per-class; bypassing means an extra instantiation per call. More importantly, it breaks the convention — other devs look for `use ConfigServiceTrait` and don't find it.337- **Don't put business logic in a helper** — helpers are *utilities* (formatters, normalizers, lookups). Business logic that involves the domain belongs in a Service (see `services` skill).338- **`AuthUserTrait` only works inside a request lifecycle.** In console commands or migrations, the session isn't bound — `getAuthUser()->getUserId()` returns null. Either pass a user ID as a command argument or work without one.339- **`getEntityManager()` is fine in any context** that's run after `Framework` is bootstrapped (which is everywhere except very early install bootstrap). Use it freely.340- **`use`-ing two traits with the same method name is a fatal error in PHP**. Rare here because OHRM trait method names are distinctive, but if you ever see `Trait method getX has not been applied`, you've got a name collision — rename one of the methods.341- **Helpers are stateless.** Don't store request-specific data on a helper service — different callers might share the same instance. If you need request state, use the service itself, not a helper.342- **`ServiceContainerTrait` is internal plumbing** — don't `use` it directly from feature code. Use the higher-level traits that wrap container access.