AI extended workkit
This commit is contained in:
parent
4b61653d2d
commit
84cc1d15d4
|
|
@ -16,21 +16,27 @@
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Blax\\Workkit\\": "src"
|
"Blax\\Workkit\\": "src"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0",
|
"php": ">=8.0",
|
||||||
"laravel/framework": "*"
|
"laravel/framework": "*",
|
||||||
|
"spatie/once": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/pint": "^1.22"
|
"laravel/pint": "^1.22"
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"providers": []
|
"providers": [
|
||||||
|
"Blax\\Workkit\\WorkkitServiceProvider"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Workkit;
|
||||||
|
|
||||||
|
class WorkkitServiceProvider extends \Illuminate\Support\ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register the service provider.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap the application events.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use JsonException;
|
||||||
|
|
||||||
|
class IncompleteJsonService
|
||||||
|
{
|
||||||
|
private $parsers = [];
|
||||||
|
|
||||||
|
private $lastParseReminding = null;
|
||||||
|
|
||||||
|
private $onExtraToken;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->parsers = array_fill_keys([' ', "\r", "\n", "\t"], 'parseSpace');
|
||||||
|
$this->parsers['['] = 'parseArray';
|
||||||
|
$this->parsers['{'] = 'parseObject';
|
||||||
|
$this->parsers['"'] = 'parseString';
|
||||||
|
$this->parsers['t'] = 'parseTrue';
|
||||||
|
$this->parsers['f'] = 'parseFalse';
|
||||||
|
$this->parsers['n'] = 'parseNull';
|
||||||
|
|
||||||
|
foreach (str_split('0123456789.-') as $char) {
|
||||||
|
$this->parsers[$char] = 'parseNumber';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->onExtraToken = function ($text, $data, $reminding) {
|
||||||
|
echo 'Parsed JSON with extra tokens: '.json_encode(['text' => $text, 'data' => $data, 'reminding' => $reminding]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse($s, bool $associative = true)
|
||||||
|
{
|
||||||
|
if (strlen($s) >= 1) {
|
||||||
|
try {
|
||||||
|
return json_decode($s, $associative, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
[$data, $reminding] = $this->parseAny($s, $e);
|
||||||
|
$this->lastParseReminding = $reminding;
|
||||||
|
if ($this->onExtraToken && $reminding) {
|
||||||
|
call_user_func($this->onExtraToken, $s, $data, $reminding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return json_decode('{}', $associative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseAny($s, $e)
|
||||||
|
{
|
||||||
|
if (! $s) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
$parser = $this->parsers[$s[0]] ?? null;
|
||||||
|
if (! $parser) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->{$parser}($s, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseSpace($s, $e)
|
||||||
|
{
|
||||||
|
return $this->parseAny(trim($s), $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseArray($s, $e)
|
||||||
|
{
|
||||||
|
$s = substr($s, 1); // skip starting '['
|
||||||
|
$acc = [];
|
||||||
|
$s = trim($s);
|
||||||
|
while ($s) {
|
||||||
|
if ($s[0] == ']') {
|
||||||
|
$s = substr($s, 1); // skip ending ']'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[$res, $s] = $this->parseAny($s, $e);
|
||||||
|
$acc[] = $res;
|
||||||
|
$s = trim($s);
|
||||||
|
if (strpos($s, ',') === 0) {
|
||||||
|
$s = substr($s, 1);
|
||||||
|
$s = trim($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$acc, $s];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseObject($s, $e)
|
||||||
|
{
|
||||||
|
$s = substr($s, 1); // skip starting '{'
|
||||||
|
$acc = [];
|
||||||
|
$s = trim($s);
|
||||||
|
while ($s) {
|
||||||
|
if ($s[0] == '}') {
|
||||||
|
$s = substr($s, 1); // skip ending '}'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[$key, $s] = $this->parseAny($s, $e);
|
||||||
|
$s = trim($s);
|
||||||
|
|
||||||
|
if (! $s || $s[0] == '}') {
|
||||||
|
$acc[$key] = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($s[0] != ':') {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s = substr($s, 1); // skip ':'
|
||||||
|
$s = trim($s);
|
||||||
|
|
||||||
|
if (! $s || in_array($s[0], [',', '}'])) {
|
||||||
|
$acc[$key] = null;
|
||||||
|
if (strpos($s, ',') === 0) {
|
||||||
|
$s = substr($s, 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$value, $s] = $this->parseAny($s, $e);
|
||||||
|
$acc[$key] = $value;
|
||||||
|
$s = trim($s);
|
||||||
|
if (strpos($s, ',') === 0) {
|
||||||
|
$s = substr($s, 1);
|
||||||
|
$s = trim($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$acc, $s];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseString($s, $e)
|
||||||
|
{
|
||||||
|
$end = strpos($s, '"', 1);
|
||||||
|
while ($end !== false && $s[$end - 1] == '\\') { // Handle escaped quotes
|
||||||
|
$end = strpos($s, '"', $end + 1);
|
||||||
|
}
|
||||||
|
if ($end === false) {
|
||||||
|
// Return the incomplete string without the opening quote
|
||||||
|
return [substr($s, 1), ''];
|
||||||
|
}
|
||||||
|
$strVal = substr($s, 0, $end + 1);
|
||||||
|
$s = substr($s, $end + 1);
|
||||||
|
|
||||||
|
return [json_decode($strVal), $s];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseNumber($s, $e)
|
||||||
|
{
|
||||||
|
$i = 0;
|
||||||
|
while ($i < strlen($s) && strpos('0123456789.-', $s[$i]) !== false) {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$numStr = substr($s, 0, $i);
|
||||||
|
$s = substr($s, $i);
|
||||||
|
if ($numStr == '' || substr($numStr, -1) == '.' || substr($numStr, -1) == '-') {
|
||||||
|
// Return the incomplete number as is
|
||||||
|
return [$numStr, ''];
|
||||||
|
}
|
||||||
|
if (strpos($numStr, '.') !== false || strpos($numStr, 'e') !== false || strpos($numStr, 'E') !== false) {
|
||||||
|
$num = floatval($numStr);
|
||||||
|
} else {
|
||||||
|
$num = intval($numStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$num, $s];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseTrue($s, $e)
|
||||||
|
{
|
||||||
|
if (substr($s, 0, 4) == 'true') {
|
||||||
|
return [true, substr($s, 4)];
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseFalse($s, $e)
|
||||||
|
{
|
||||||
|
if (substr($s, 0, 5) == 'false') {
|
||||||
|
return [false, substr($s, 5)];
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseNull($s, $e)
|
||||||
|
{
|
||||||
|
if (substr($s, 0, 4) == 'null') {
|
||||||
|
return [null, substr($s, 4)];
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Workkit\Services;
|
||||||
|
|
||||||
|
use App\Services\IncompleteJsonService;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class MiscService
|
||||||
|
{
|
||||||
|
public static function asPaginated(
|
||||||
|
$paginated,
|
||||||
|
$resource_class,
|
||||||
|
array $meta = []
|
||||||
|
) {
|
||||||
|
$data = $paginated->toArray();
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'data' => $resource_class::collection($paginated),
|
||||||
|
'meta' => [
|
||||||
|
'from' => @$data['from'],
|
||||||
|
'to' => @$data['to'],
|
||||||
|
'total' => @$data['total'],
|
||||||
|
'last_page' => @$data['last_page'],
|
||||||
|
'current_page' => @$data['current_page'],
|
||||||
|
'options' => (object) request('options'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($meta) {
|
||||||
|
$payload['meta'] = array_merge($payload['meta'], $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function bytesToHuman($bytes)
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
for ($i = 0; $bytes > 1024; $i++) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deterministicEncrypt($data)
|
||||||
|
{
|
||||||
|
return base64_encode(openssl_encrypt($data, 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deterministicDecrypt($encrypted)
|
||||||
|
{
|
||||||
|
return openssl_decrypt(base64_decode($encrypted), 'AES-128-ECB', config('app.key'), OPENSSL_RAW_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function logExecutionTime(
|
||||||
|
string $logtext,
|
||||||
|
$callable = null
|
||||||
|
) {
|
||||||
|
$start = microtime(true);
|
||||||
|
|
||||||
|
if (!$callable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $callable();
|
||||||
|
$end = microtime(true);
|
||||||
|
|
||||||
|
$executionTime = $end - $start;
|
||||||
|
|
||||||
|
Log::debug($logtext, [
|
||||||
|
'execution_time' => $executionTime
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getIpInformation($ip)
|
||||||
|
{
|
||||||
|
return once(function () use ($ip) {
|
||||||
|
return cache()->flexible('ipapi-' . $ip, [60 * 60 * 24 * 2, 60 * 60 * 24 * 7], function () use ($ip) {
|
||||||
|
$response = Http::get("https://ipapi.co/{$ip}/json/");
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->json();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function countryToCode($country_long): ?string
|
||||||
|
{
|
||||||
|
return match (str()->lower($country_long)) {
|
||||||
|
'deutschland' => 'de',
|
||||||
|
'österreich' => 'at',
|
||||||
|
'schweiz' => 'ch',
|
||||||
|
'spanien' => 'es',
|
||||||
|
'luxemburg' => 'lu',
|
||||||
|
'estland' => 'ee',
|
||||||
|
'belgien' => 'be',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function codeToCountry($country_code, string|null $locale = null)
|
||||||
|
{
|
||||||
|
$country_code = str()->lower($country_code);
|
||||||
|
$locale ??= app()->getLocale();
|
||||||
|
|
||||||
|
if ($locale === 'de') {
|
||||||
|
return match ($country_code) {
|
||||||
|
'de' => 'Deutschland',
|
||||||
|
'at' => 'Österreich',
|
||||||
|
'ch' => 'Schweiz',
|
||||||
|
'es' => 'Spanien',
|
||||||
|
'lu' => 'Luxemburg',
|
||||||
|
'ee' => 'Estland',
|
||||||
|
'be' => 'Belgien',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($locale === 'es') {
|
||||||
|
return match ($country_code) {
|
||||||
|
'de' => 'Alemania',
|
||||||
|
'at' => 'Austria',
|
||||||
|
'ch' => 'Suiza',
|
||||||
|
'es' => 'España',
|
||||||
|
'lu' => 'Luxemburgo',
|
||||||
|
'ee' => 'Estonia',
|
||||||
|
'be' => 'Bélgica',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($locale === 'uk') {
|
||||||
|
return match ($country_code) {
|
||||||
|
'de' => 'Німеччина',
|
||||||
|
'at' => 'Австрія',
|
||||||
|
'ch' => 'Швейцарія',
|
||||||
|
'es' => 'Іспанія',
|
||||||
|
'lu' => 'Люксембург',
|
||||||
|
'ee' => 'Естонія',
|
||||||
|
'be' => 'Бельгія',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($country_code) {
|
||||||
|
'de' => 'Germany',
|
||||||
|
'at' => 'Austria',
|
||||||
|
'ch' => 'Switzerland',
|
||||||
|
'es' => 'Spain',
|
||||||
|
'lu' => 'Luxembourg',
|
||||||
|
'ee' => 'Estonia',
|
||||||
|
'be' => 'Belgium',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parseIncompleteJson(
|
||||||
|
string $json,
|
||||||
|
bool $associative = true
|
||||||
|
): array|object|null {
|
||||||
|
return (new IncompleteJsonService())->parse($json, $associative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
trait HasMeta
|
||||||
|
{
|
||||||
|
public function getMeta(): object
|
||||||
|
{
|
||||||
|
return (object) $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function updateMetaKey($key, $value, bool $update = true): self
|
||||||
|
{
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$meta->{$key} = $value;
|
||||||
|
$this->meta = (object) $meta;
|
||||||
|
|
||||||
|
if ($update) {
|
||||||
|
$this->update(['meta' => $this->meta]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use App\Services\IncompleteJsonService;
|
||||||
|
|
||||||
|
trait HasMetaTranslation
|
||||||
|
{
|
||||||
|
use HasMeta;
|
||||||
|
|
||||||
|
protected array $__localizedFallbackGuard = [];
|
||||||
|
|
||||||
|
public function getMeta(): object
|
||||||
|
{
|
||||||
|
$r = (object) $this->meta;
|
||||||
|
$r->i18n ??= (object) [];
|
||||||
|
|
||||||
|
if (is_array($r->i18n)) {
|
||||||
|
$r->i18n = (object) $r->i18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (config('app.i18n.supporting') as $lang => $longlang) {
|
||||||
|
if ($lang == 'en') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($r->i18n->{$lang})) {
|
||||||
|
$r->i18n->{$lang} = (object) [];
|
||||||
|
} elseif (is_array($r->i18n->{$lang})) {
|
||||||
|
// Normalize any existing array locale bucket to object
|
||||||
|
$r->i18n->{$lang} = (object) $r->i18n->{$lang};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocalized($key, string|null $locale = null, bool $allowAttrFallback = true)
|
||||||
|
{
|
||||||
|
$locale = $locale ?: request()->get('locale') ?: app()->getLocale() ?: 'en';
|
||||||
|
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$i18n = $meta->i18n ?? null;
|
||||||
|
|
||||||
|
$get = function ($container, $prop) {
|
||||||
|
if ($container === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (is_array($container)) {
|
||||||
|
return $container[$prop] ?? null;
|
||||||
|
}
|
||||||
|
if (is_object($container)) {
|
||||||
|
return $container->{$prop} ?? null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build recursive fallback chain from config
|
||||||
|
$fallbackMap = (array) config('app.i18n.fallback', []);
|
||||||
|
$chain = [];
|
||||||
|
$visited = [];
|
||||||
|
|
||||||
|
$expand = function ($loc) use (&$expand, &$chain, &$visited, $fallbackMap) {
|
||||||
|
if (!$loc || isset($visited[$loc])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$visited[$loc] = true;
|
||||||
|
$chain[] = $loc;
|
||||||
|
if (!isset($fallbackMap[$loc])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$targets = (array) $fallbackMap[$loc];
|
||||||
|
foreach ($targets as $t) {
|
||||||
|
$expand($t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$expand($locale);
|
||||||
|
|
||||||
|
// Always ensure 'en' ends up as last resort if not already included
|
||||||
|
if (!in_array('en', $chain, true)) {
|
||||||
|
$expand('en');
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = null;
|
||||||
|
foreach ($chain as $loc) {
|
||||||
|
// Prefer normalized meta object path
|
||||||
|
$candidate = $get($get($i18n, $loc), $key);
|
||||||
|
if ($candidate === null || $candidate === '') {
|
||||||
|
// Try raw meta structure if different
|
||||||
|
$candidate = $get($get($get($this->meta ?? null, 'i18n'), $loc), $key);
|
||||||
|
}
|
||||||
|
if ($candidate !== null && $candidate !== '') {
|
||||||
|
$value = $candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional model attribute fallback (raw attribute array only to avoid recursion)
|
||||||
|
if (($value === null || $value === '') && $allowAttrFallback) {
|
||||||
|
$attr = $this->attributes[$key] ?? null;
|
||||||
|
if ($attr !== null && $attr !== '') {
|
||||||
|
$value = $attr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply model casts (e.g., json) when present so localized values respect attribute casting
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
try {
|
||||||
|
$casts = method_exists($this, 'getCasts') ? $this->getCasts() : (property_exists($this, 'casts') ? (array) $this->casts : []);
|
||||||
|
$cast = $casts[$key] ?? null;
|
||||||
|
$castStr = is_string($cast) ? strtolower($cast) : '';
|
||||||
|
|
||||||
|
$isJsonLike = (function ($t) {
|
||||||
|
if (!$t) return false;
|
||||||
|
$t = strtolower($t);
|
||||||
|
return $t === 'array' || $t === 'object' || $t === 'collection' || str_contains($t, 'json');
|
||||||
|
})($castStr);
|
||||||
|
|
||||||
|
if ($isJsonLike) {
|
||||||
|
if (method_exists($this, 'hasCast') && method_exists($this, 'castAttribute') && $this->hasCast($key)) {
|
||||||
|
// Let Eloquent do the casting for consistency
|
||||||
|
$value = $this->castAttribute($key, $value);
|
||||||
|
} else {
|
||||||
|
// Minimal manual handling for json/array/object casts
|
||||||
|
if (is_string($value)) {
|
||||||
|
$assoc = ($castStr === 'array' || str_contains($castStr, 'json'));
|
||||||
|
$decoded = json_decode($value, $assoc);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$value = $decoded;
|
||||||
|
}
|
||||||
|
} elseif (is_object($value) && ($castStr === 'array' || str_contains($castStr, 'json'))) {
|
||||||
|
$value = json_decode(json_encode($value), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Swallow casting errors to avoid breaking localization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocalized($key, $value, string|null $locale = null, bool $save = false)
|
||||||
|
{
|
||||||
|
$locale = $locale ?: request()->get('locale') ?: app()->getLocale() ?: 'en';
|
||||||
|
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$i18n = (object) ($meta->i18n ?? []);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$locale == 'en'
|
||||||
|
&& array_key_exists($key, $this->attributes)
|
||||||
|
) {
|
||||||
|
$this->attributes[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($i18n->{$locale})) {
|
||||||
|
$i18n->{$locale} = (object) [];
|
||||||
|
} elseif (is_array($i18n->{$locale})) {
|
||||||
|
// Normalize existing array to object before property assignment
|
||||||
|
$i18n->{$locale} = (object) $i18n->{$locale};
|
||||||
|
}
|
||||||
|
|
||||||
|
$i18n->{$locale}->{$key} = $value;
|
||||||
|
|
||||||
|
$meta->i18n = $i18n;
|
||||||
|
$this->meta = (object) $meta;
|
||||||
|
|
||||||
|
if ($save) {
|
||||||
|
$this->update(['meta' => $this->meta]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wipeTranslation($locale = 'all')
|
||||||
|
{
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$i18n = (object) ($meta->i18n ?? []);
|
||||||
|
|
||||||
|
if ($locale === 'all') {
|
||||||
|
// Wipe all translations
|
||||||
|
$i18n = (object) [];
|
||||||
|
} elseif (isset($i18n->{$locale})) {
|
||||||
|
// Wipe specific locale
|
||||||
|
$i18n->{$locale} = (object) [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta->i18n = $i18n;
|
||||||
|
$this->meta = (object) $meta;
|
||||||
|
|
||||||
|
$this->update(['meta' => $this->meta]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all supported languages that are still an empty object
|
||||||
|
* */
|
||||||
|
public function getMissingTranslationLanguagesAttribute()
|
||||||
|
{
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$i18n = (object) ($meta->i18n ?? []);
|
||||||
|
|
||||||
|
$missing = [];
|
||||||
|
|
||||||
|
foreach (config('app.i18n.supporting') as $lang => $longlang) {
|
||||||
|
if ($lang == 'en') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $i18n->{$lang} ?? null;
|
||||||
|
|
||||||
|
if ($container === null) {
|
||||||
|
$missing[] = $lang;
|
||||||
|
} elseif (is_array($container) && count($container) === 0) {
|
||||||
|
$missing[] = $lang;
|
||||||
|
} elseif (is_object($container) && count(get_object_vars($container)) === 0) {
|
||||||
|
$missing[] = $lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function getMissingTranslationKeysAttribute()
|
||||||
|
{
|
||||||
|
// assume static variable $translatables, where keys of translatable fillables are listed
|
||||||
|
$missing = [];
|
||||||
|
$translatables = property_exists($this, 'translatables') ? (array) $this->translatables : [];
|
||||||
|
$supported_langs = array_keys(config('app.i18n.supporting', []));
|
||||||
|
|
||||||
|
// get translatables from meta i18n as well
|
||||||
|
$meta = $this->getMeta();
|
||||||
|
$i18n_keys = array_keys((array) (((object) ($meta->i18n ?? []))?->en ?? []));
|
||||||
|
$translatables = array_unique(array_merge($translatables, $i18n_keys));
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($translatables as $key) {
|
||||||
|
foreach ($supported_langs as $lang) {
|
||||||
|
if ($lang == 'en') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->getLocalized($key, $lang, false);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$value == null
|
||||||
|
|| $value == ''
|
||||||
|
|| $value == $this->getLocalized($key, 'en', false)
|
||||||
|
) {
|
||||||
|
$missing[$key][] = $lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function getMissingKeyLanguagesAttribute()
|
||||||
|
{
|
||||||
|
$missing = $this->missing_translation_keys;
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($missing as $key => $langs) {
|
||||||
|
foreach ($langs as $lang) {
|
||||||
|
$result[$lang][] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic dynamic attribute fallback to localization (replaces specific title accessor)
|
||||||
|
public function __get($key)
|
||||||
|
{
|
||||||
|
$value = parent::__get($key);
|
||||||
|
|
||||||
|
if (($value === null || $value === '')
|
||||||
|
&& !in_array($key, ['meta', 'i18n'])
|
||||||
|
&& empty($this->__localizedFallbackGuard[$key])
|
||||||
|
) {
|
||||||
|
$this->__localizedFallbackGuard[$key] = true;
|
||||||
|
try {
|
||||||
|
$localized = $this->getLocalized($key, null, false);
|
||||||
|
} finally {
|
||||||
|
unset($this->__localizedFallbackGuard[$key]);
|
||||||
|
}
|
||||||
|
if ($localized !== null && $localized !== '') {
|
||||||
|
return $localized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWhereI18n($query, $key, $value, $locale = null)
|
||||||
|
{
|
||||||
|
if ($locale) {
|
||||||
|
$locale = $locale ?: app()->getLocale() ?: 'en';
|
||||||
|
return $query->where("meta->i18n->{$locale}->{$key}", $value);
|
||||||
|
} else {
|
||||||
|
return $query->where(function ($q) use ($key, $value) {
|
||||||
|
foreach (array_keys(config('app.i18n.supporting', [])) as $locale) {
|
||||||
|
$q->orWhere("meta->i18n->{$locale}->{$key}", $value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeOrWhereI18n($query, $key, $value, $locale = null)
|
||||||
|
{
|
||||||
|
if ($locale) {
|
||||||
|
$locale = $locale ?: app()->getLocale() ?: 'en';
|
||||||
|
return $query->orWhere("meta->i18n->{$locale}->{$key}", $value);
|
||||||
|
} else {
|
||||||
|
return $query->orWhere(function ($q) use ($key, $value) {
|
||||||
|
foreach (array_keys(config('app.i18n.supporting', [])) as $locale) {
|
||||||
|
$q->orWhere("meta->i18n->{$locale}->{$key}", $value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Blax\Workkit\Services\MiscService;
|
||||||
|
|
||||||
|
if (!function_exists('misc')) {
|
||||||
|
function misc(): MiscService
|
||||||
|
{
|
||||||
|
static $instance;
|
||||||
|
|
||||||
|
if (!$instance) {
|
||||||
|
$instance = new MiscService();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue