yasser-elgammal / green-locale
Localized model attributes for Green Framework Web & API.
Requires
- php: ^8.2
- doctrine/dbal: ^4.0
- respect/validation: ^2.3
Requires (Dev)
- phpunit/phpunit: ^12.5
This package is not auto-updated.
Last update: 2026-05-31 13:40:54 UTC
README
This package provides Localization for Green Framework.
Localized JSON attributes for Green Framework models.
green-locale lets a model store translated attributes such as name, description, or slug in a single JSON column, then read the right value for the current locale with explicit model methods.
Contents
- Requirements
- Installation
- Configuration
- Bootstrapping
- Database columns
- Model setup
- Reading locale values
- Writing locale values
- Persisting model changes
- Locale resolution
- Querying localized values
- Validation rules
- Events
- Helpers
- Testing
- Full example
- Troubleshooting
- Contributing
Requirements
- PHP
^8.2 - Green Framework skeleton
- Doctrine DBAL, already used by Green
- Respect Validation, used for the package validation rules
Installation
Install it normally:
composer require yasser-elgammal/green-locale
Configuration
Create config/locale.php in your Green skeleton:
<?php return [ 'default_locale' => $_ENV['APP_LOCALE'] ?? 'en', 'fallback_locale' => $_ENV['APP_FALLBACK_LOCALE'] ?? 'en', 'available_locales' => [ 'en', 'ar', ], 'resolver' => ['query', 'session', 'request', 'config'], 'strict' => false, ];
Config Options
default_locale
The locale used when no resolver finds a locale.
fallback_locale
The locale used when a translated attribute does not have a value for the current locale.
available_locales
The list of locales your application supports.
resolver
The locale resolver driver or resolver chain. Resolvers are executed in the exact order they are defined in the array (from left to right). The first resolver that returns an available locale wins.
Supported drivers (and their typical priority):
query: Highest priority. Reads the locale from the URL (e.g.,?locale=ar).session: Readssession()->get('locale')or$_SESSION['locale']if the user changed their preference previously.request: Reads theAccept-LanguageHTTP header to detect the browser's preferred language.config: Lowest priority. Returns thedefault_localeif no other method succeeds.
strict
When false, missing locale values return the fallback value or null.
When true, missing locale values throw MissingLocaleValueException.
Bootstrapping
Boot the package once during application startup.
In public/index.php, after loading Composer and defining BASE_PATH, add:
use YasserElgammal\GreenLocale\LocaleServiceProvider; LocaleServiceProvider::boot();
Example:
require_once __DIR__ . '/../vendor/autoload.php'; define('BASE_PATH', realpath(__DIR__ . '/../')); $dotenv = Dotenv\Dotenv::createImmutable(BASE_PATH); $dotenv->safeLoad(); use YasserElgammal\GreenLocale\LocaleServiceProvider; LocaleServiceProvider::boot();
Database Columns
Localized attributes are stored as JSON strings in normal database columns.
Example stored value for name:
{
"en": "Phone",
"ar": "هاتف"
}
Model Setup
Use HasLocales on any model that has localized JSON attributes.
<?php namespace App\Models; use YasserElgammal\Green\Database\Model; use YasserElgammal\GreenLocale\Concerns\HasLocales; class Product extends Model { use HasLocales; protected string $table = 'products'; protected string $primaryKey = 'id'; protected array $localeAttributes = [ 'name', 'description', 'slug', ]; }
Only attributes listed in $localeAttributes can be used with locale methods. Calling locale methods on undeclared attributes throws LocaleAttributeNotDeclared.
Reading Locale Values
Read the value for the current locale:
$product->localeValue('name');
Read a specific locale:
$product->localeValue('name', 'ar');
Read all locale values:
$product->localeValues('name');
Check if a locale value exists and is not empty:
$product->hasLocaleValue('name', 'en');
Fallback behavior:
// Current locale: ar // Fallback locale: en // Stored: {"en": "Phone"} $product->localeValue('name'); // "Phone"
Writing Locale Values
Set one locale value:
$product->putLocaleValue('name', 'ar', 'هاتف');
Set multiple values and replace the whole locale map:
$product->syncLocaleValues('name', [ 'en' => 'Phone', 'ar' => 'هاتف', ]);
Remove one locale value:
$product->forgetLocaleValue('name', 'ar');
All writing methods return $this, so chaining is supported:
$product ->putLocaleValue('name', 'en', 'Phone') ->putLocaleValue('name', 'ar', 'هاتف');
Persisting Model Changes
Green models are DTO-style objects. Calling putLocaleValue() updates the model attribute in memory. You still need to persist the changed attribute through your table gateway.
$table = new ProductTable(); $product = $table->fetchById($id); $product->putLocaleValue('name', 'ar', 'هاتف'); $table->update($id, [ 'name' => $product->getAttribute('name'), ]);
Full controller example:
use YasserElgammal\Green\Http\Request; use YasserElgammal\Green\Http\JsonResponse; #[Route('POST', '/api/products/{id}/locale')] public function updateLocale(Request $request, int $id): JsonResponse { $table = new ProductTable(); $product = $table->fetchById($id); $product->putLocaleValue('name', $request->input('locale'), $request->input('name')); $table->update($id, [ 'name' => $product->getAttribute('name'), ]); return api()->item($product, new ProductTransformer()); }
Locale Resolution
The current locale is managed by LocaleManager.
use YasserElgammal\GreenLocale\LocaleManager; $manager = LocaleManager::getInstance(); $manager->current(); $manager->fallback(); $manager->available(); $manager->isAvailable('ar');
Set the current locale manually:
LocaleManager::getInstance()->setLocale('ar');
Set the fallback locale manually:
LocaleManager::getInstance()->setFallbackLocale('en');
If you already have an app middleware that stores the locale in the session, sync it into LocaleManager:
use YasserElgammal\GreenLocale\LocaleManager; $locale = session()->get('locale'); $manager = LocaleManager::getInstance(); if (is_string($locale) && $manager->isAvailable($locale)) { $manager->setLocale($locale); }
Querying Localized Values
Use LocaleQueryBuilder with Green table builders.
use YasserElgammal\GreenLocale\LocaleQueryBuilder; $table = new ProductTable(); $qb = $table->builder(); LocaleQueryBuilder::whereLocale($qb, 'name', 'en', 'Phone'); $products = $table->fetchAllFromBuilder($qb);
LIKE query:
LocaleQueryBuilder::whereLocaleLike($qb, 'name', 'en', '%Pho%');
Order by a locale value:
LocaleQueryBuilder::orderByLocale($qb, 'name', 'ar', 'ASC');
Generated SQL uses JSON extraction:
JSON_UNQUOTE(JSON_EXTRACT(`name`, '$."en"')) = :locale_value
Validation Rules
The package includes Respect Validation rules.
LocaleArrayRule
Validates that the input is an associative array with valid locale keys.
use YasserElgammal\GreenLocale\Validation\LocaleArrayRule; $rule = new LocaleArrayRule(); $rule->validate([ 'en' => 'Phone', 'ar' => 'هاتف', ]); // true
Valid locale key examples:
enaren_US
LocaleRequiredRule
Validates that required locales exist and are not empty.
use YasserElgammal\GreenLocale\Validation\LocaleRequiredRule; $rule = new LocaleRequiredRule(['en', 'ar']); $rule->validate([ 'en' => 'Phone', 'ar' => 'هاتف', ]); // true
LocaleExistsRule
Validates that all locale keys are available locales.
use YasserElgammal\GreenLocale\Validation\LocaleExistsRule; $rule = new LocaleExistsRule(['en', 'ar']); $rule->validate([ 'en' => 'Phone', 'fr' => 'Telephone', ]); // false
Example inside a payload:
use Respect\Validation\Validator as v; use YasserElgammal\Green\Payload\Payload; use YasserElgammal\GreenLocale\Validation\LocaleArrayRule; use YasserElgammal\GreenLocale\Validation\LocaleExistsRule; use YasserElgammal\GreenLocale\Validation\LocaleRequiredRule; class StoreProductPayload extends Payload { public function rules(): array { return [ 'name' => v::allOf( new LocaleArrayRule(), new LocaleRequiredRule(['en']), new LocaleExistsRule(['en', 'ar']) ), ]; } }
Events
The package does not use a hidden event bus. Locale events are collected on the model and can be dispatched explicitly.
Events:
LocaleValueStoredLocaleValueForgottenLocaleValuesSynced
Read pending events:
$events = $product->localeEvents();
Dispatch and clear pending events:
$events = $product->dispatchLocaleEvents(function (object $event): void { // Send to your logger, queue, or application event system. });
If no callback is passed, dispatchLocaleEvents() simply returns the events and clears the pending list.
$events = $product->dispatchLocaleEvents();
Helpers
The package autoloads helper functions.
locale_manager(); current_locale(); fallback_locale();
Examples:
$locale = current_locale(); $fallback = fallback_locale(); locale_manager()->setLocale('ar');
Transformers
Localized values are usually resolved in transformers:
use YasserElgammal\Green\Database\Model; class ProductTransformer { public function transform(Model $model): array { return [ 'id' => (int) $model->id, 'name' => $model->localeValue('name'), 'description' => $model->localeValue('description'), 'all_names' => $model->localeValues('name'), ]; } }
Twig Usage
If your model is passed to Twig, call the explicit model method:
{{ product.localeValue('name') }}
To expose the current locale:
return view('products/index', [ 'products' => $products, 'locale' => current_locale(), ]);
Testing
Run package tests from the Green skeleton:
vendor/bin/phpunit -c ../green-locale/phpunit.xml
Run the Green skeleton tests:
vendor/bin/phpunit tests
Validate Composer files:
composer validate --no-check-publish
cd ../green-locale
composer validate --no-check-publish
Full Example
1. Create Table
Generate a new migration:
php green make:migration CreateProductsTable
namespace Database\Migrations; use YasserElgammal\Green\Database\Migrations\Migration; use YasserElgammal\Green\Database\Schema\Blueprint; use YasserElgammal\Green\Database\Schema\Schema; class CreateProductsTable extends Migration { public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); $table->text('name'); $table->text('description')->nullable(); $table->text('slug')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('products'); } }
2. Create Model
namespace App\Models; use YasserElgammal\Green\Database\Model; use YasserElgammal\GreenLocale\Concerns\HasLocales; class Product extends Model { use HasLocales; protected string $table = 'products'; protected string $primaryKey = 'id'; protected array $localeAttributes = [ 'name', 'description', ]; }
3. Create Table Gateway
namespace App\Tables; use App\Models\Product; use YasserElgammal\Green\Database\Table; class ProductTable extends Table { public function __construct() { parent::__construct(new Product()); } }
4. Insert Product
$table = new ProductTable(); $product = $table->insert([ 'name' => json_encode([ 'en' => 'Phone', 'ar' => 'هاتف', ], JSON_UNESCAPED_UNICODE), 'description' => json_encode([ 'en' => 'Smart device', 'ar' => 'جهاز ذكي', ], JSON_UNESCAPED_UNICODE), ]);
5. Read Product
LocaleManager::getInstance()->setLocale('ar'); echo $product->localeValue('name'); // هاتف
6. Update Product Locale
$product->putLocaleValue('name', 'ar', 'هاتف جديد'); $table->update($product->id, [ 'name' => $product->getAttribute('name'), ]);
7. Query Product
$qb = $table->builder(); LocaleQueryBuilder::whereLocale($qb, 'name', 'en', 'Phone'); $products = $table->fetchAllFromBuilder($qb);
Troubleshooting
Locale Always Falls Back
Check that:
LocaleServiceProvider::boot()is called- The requested locale exists in
available_locales - The resolver chain includes the source you are using, such as
queryorsession
LocaleAttributeNotDeclared
Add the attribute to $localeAttributes:
protected array $localeAttributes = [ 'name', 'description', ];
Missing Values Return Null
If strict is false, missing values return fallback or null.
If you want missing values to throw exceptions:
'strict' => true,
JSON Query Does Not Work
LocaleQueryBuilder uses JSON functions:
JSON_UNQUOTE(JSON_EXTRACT(...))
Make sure your database supports these functions. MySQL supports them. SQLite support depends on the JSON extension being available.
Contributing
Contributions are welcome!