Skip to content

Commit 0ab283c

Browse files
committed
Extract MutationTrait responsibilities into dedicated helper traits
1 parent e49a9a5 commit 0ab283c

4 files changed

Lines changed: 426 additions & 383 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace PSFS\base\types\traits\Api;
4+
5+
use Propel\Runtime\ActiveQuery\ModelCriteria;
6+
use Propel\Runtime\Map\ColumnMap;
7+
use Propel\Runtime\Map\TableMap;
8+
use PSFS\base\config\Config;
9+
use PSFS\base\exception\ApiException;
10+
use PSFS\base\Request;
11+
use PSFS\base\types\Api;
12+
13+
trait MutationExtraColumnsTrait
14+
{
15+
/**
16+
* @throws ApiException
17+
*/
18+
protected function addPkToList()
19+
{
20+
foreach ($this->getPkDbName() as $extraColumn => $columnName) {
21+
$this->extraColumns[$extraColumn] = $columnName;
22+
}
23+
}
24+
25+
/**
26+
* @param TableMap $tableMap
27+
*/
28+
private function addClassListName(TableMap $tableMap)
29+
{
30+
$segments = [];
31+
foreach ($tableMap->getPrimaryKeys() as $pk) {
32+
$segments[] = $pk->getFullyQualifiedName();
33+
}
34+
35+
$this->extraColumns[$this->buildClassListNameExpression((string)$tableMap->getPhpName(), $segments)] = Api::API_LIST_NAME_FIELD;
36+
}
37+
38+
/**
39+
* @param array<int, string> $segments
40+
*/
41+
private function buildClassListNameExpression(string $phpName, array $segments): string
42+
{
43+
if (count($segments) === 0) {
44+
return 'CONCAT("' . $phpName . ' #")';
45+
}
46+
47+
return 'CONCAT("' . $phpName . ' #", ' . implode(', "|", ', $segments) . ')';
48+
}
49+
50+
protected function addDefaultListField()
51+
{
52+
if (!in_array(Api::API_LIST_NAME_FIELD, array_values($this->extraColumns), true)) {
53+
$tableMap = $this->getTableMap();
54+
$column = $this->resolveDefaultListColumn($tableMap);
55+
if (null !== $column) {
56+
$this->extraColumns[$column->getFullyQualifiedName()] = Api::API_LIST_NAME_FIELD;
57+
} else {
58+
$this->addClassListName($tableMap);
59+
}
60+
}
61+
}
62+
63+
/**
64+
* @param ModelCriteria $query
65+
* @param mixed $action
66+
* @throws ApiException
67+
*/
68+
private function addExtraColumns(ModelCriteria &$query, $action)
69+
{
70+
$this->prepareLegacyListExtraColumns((string)$action);
71+
if (empty($this->extraColumns)) {
72+
return;
73+
}
74+
75+
$fields = $this->resolveRequestedExtraFields();
76+
foreach ($this->extraColumns as $expression => $columnName) {
77+
if ($this->shouldIncludeExtraColumn($columnName, $fields)) {
78+
$query->withColumn($expression, $columnName);
79+
}
80+
}
81+
}
82+
83+
private function prepareLegacyListExtraColumns(string $action): void
84+
{
85+
if (Api::API_ACTION_LIST !== $action) {
86+
return;
87+
}
88+
// Legacy tokens kept for compatibility (`__name__`, `__pk`).
89+
// Planned future cleanup can remove them behind a versioned contract switch.
90+
$this->addDefaultListField();
91+
$this->addPkToList();
92+
}
93+
94+
/**
95+
* @param array<int, string> $fields
96+
*/
97+
private function shouldIncludeExtraColumn(string $columnName, array $fields): bool
98+
{
99+
return empty($fields) || in_array($columnName, $fields, true);
100+
}
101+
102+
/**
103+
* @return array<string, string>
104+
*/
105+
protected function parseExtraColumns()
106+
{
107+
$columns = [];
108+
foreach ($this->extraColumns as $columnName) {
109+
$columns[$columnName] = strtolower($columnName);
110+
}
111+
return $columns;
112+
}
113+
114+
/**
115+
* @return array<int, string>
116+
*/
117+
protected function resolveRequestedExtraFields(): array
118+
{
119+
if (Config::getParam('api.extrafields.compat', true)) {
120+
return array_values($this->extraColumns);
121+
}
122+
return $this->normalizeRequestedExtraFields(
123+
(string)(Request::getInstance()->getQuery(Api::API_FIELDS_RESULT_FIELD) ?? '')
124+
);
125+
}
126+
127+
/**
128+
* @return array<int, string>
129+
*/
130+
private function normalizeRequestedExtraFields(string $returnFields): array
131+
{
132+
$fields = explode(',', $returnFields);
133+
$fields[] = self::API_MODEL_KEY_FIELD;
134+
$fields = array_values(array_filter(array_map('trim', $fields), static fn(string $field): bool => $field !== ''));
135+
136+
return array_values(array_unique($fields));
137+
}
138+
139+
private function resolveDefaultListColumn(TableMap $tableMap): ?ColumnMap
140+
{
141+
foreach (['NAME', 'TITLE', 'LABEL'] as $columnName) {
142+
if ($tableMap->hasColumn($columnName)) {
143+
return $tableMap->getColumn($columnName);
144+
}
145+
}
146+
return null;
147+
}
148+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
namespace PSFS\base\types\traits\Api;
4+
5+
use Exception;
6+
use Propel\Runtime\ActiveQuery\ModelCriteria;
7+
use Propel\Runtime\ActiveRecord\ActiveRecordInterface;
8+
use Propel\Runtime\Map\TableMap;
9+
use PSFS\base\config\Config;
10+
use PSFS\base\Logger;
11+
use PSFS\base\Request;
12+
use PSFS\base\types\Api;
13+
use PSFS\base\types\helpers\ApiHelper;
14+
use PSFS\base\types\helpers\I18nHelper;
15+
16+
trait MutationRequestHydrationTrait
17+
{
18+
protected function hydrateRequestData()
19+
{
20+
$request = Request::getInstance();
21+
$this->query = array_merge($this->query, $request->getQueryParams());
22+
$this->data = array_merge($this->data, $request->getRawData());
23+
}
24+
25+
protected function extractApiLang()
26+
{
27+
$defaultLanguage = (string)Config::getParam('default.language', 'en_US');
28+
$this->lang = Request::header(Api::HEADER_API_LANG, $defaultLanguage);
29+
}
30+
31+
/**
32+
* @param ModelCriteria $query
33+
*/
34+
protected function checkI18n(ModelCriteria &$query)
35+
{
36+
$this->extractApiLang();
37+
if (!$this->hasI18nQuerySupport($query)) {
38+
return;
39+
}
40+
$query->useI18nQuery($this->lang);
41+
$model = (string)$this->getModelNamespace();
42+
$i18nMapClass = $this->resolveI18nMapClassName($model);
43+
$modelI18nTableMap = $i18nMapClass::getTableMap();
44+
$this->appendI18nColumnsToQuery($query, $modelI18nTableMap, (string)$this->lang);
45+
}
46+
47+
protected function cleanData(array &$data)
48+
{
49+
foreach ($data as &$value) {
50+
$this->sanitizeRecursiveValue($value);
51+
}
52+
}
53+
54+
/**
55+
* @param mixed $value
56+
*/
57+
private function sanitizeRecursiveValue(mixed &$value): void
58+
{
59+
if (is_array($value)) {
60+
foreach ($value as &$nested) {
61+
$this->sanitizeRecursiveValue($nested);
62+
}
63+
return;
64+
}
65+
if (is_string($value)) {
66+
$value = $this->sanitizeString($value);
67+
}
68+
}
69+
70+
/**
71+
* @param ActiveRecordInterface $model
72+
* @param array $data
73+
*/
74+
protected function hydrateModelFromRequest(ActiveRecordInterface $model, array $data = [])
75+
{
76+
$this->cleanData($data);
77+
$model->fromArray($data, ApiHelper::getFieldTypes());
78+
$tableMap = $this->getTableMap();
79+
if (!$tableMap instanceof TableMap) {
80+
return;
81+
}
82+
try {
83+
$this->applyI18nFieldsToModel($model, $tableMap, $data, $this->resolveLocaleFromInput($data));
84+
} catch (Exception $e) {
85+
Logger::log($e->getMessage(), LOG_DEBUG);
86+
}
87+
}
88+
89+
protected function checkFieldType()
90+
{
91+
$this->fieldType = ApiHelper::getFieldTypes();
92+
}
93+
94+
protected function getBulkSavedCount(): int
95+
{
96+
return $this->bulkSavedCount;
97+
}
98+
99+
protected function sanitizeString(string $value): string
100+
{
101+
return I18nHelper::cleanHtmlAttacks($value);
102+
}
103+
104+
/**
105+
* @param array<string, mixed> $data
106+
*/
107+
protected function resolveLocaleFromInput(array $data): string
108+
{
109+
if (array_key_exists('Locale', $data)) {
110+
return (string)$data['Locale'];
111+
}
112+
if (array_key_exists('locale', $data)) {
113+
return (string)$data['locale'];
114+
}
115+
$defaultLanguage = (string)Config::getParam('default.language', 'en_US');
116+
return (string)Request::header(Api::HEADER_API_LANG, $defaultLanguage);
117+
}
118+
}

0 commit comments

Comments
 (0)