Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/src/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -5457,6 +5457,20 @@ public function getTemplateVarOutput($idnames = [], $docid = '', $published = 1,
return $output;
}

/**
* Reads the named TVs of many documents in one query, for listings that show a few
* TVs per item (cards, prices, ratings) where getTemplateVarOutput() per item is N+1.
*
* @param array $docIds Document ids, e.g. array_column(getDocumentChildren(...), 'id')
* @param array $tvNames TV names (or TV ids when every element is numeric)
* @param bool $withDefaults Fall back to the TV default_text when a value is empty or missing
* @return array [docid => [tvname => raw value]], [] when no TV matches
*/
public function getTemplateVarValues(array $docIds, array $tvNames, bool $withDefaults = true): array
{
return SiteContent::getTvValues($docIds, $tvNames, $withDefaults);
}

/**
* Returns the full table name based on db settings
*
Expand Down
82 changes: 51 additions & 31 deletions core/src/Models/SiteContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2239,42 +2239,62 @@ public function scopeGetRootTree($query, $depth = 0)
//return tvs array [$docid => tvs array()]
public static function getTvList($docs, $tvList = [])
{
$docsTV = [];
if (empty($docs)) {
if (empty($docs) || empty($tvList)) {
return [];
} else if (empty($tvList)) {
}

return static::getTvValues($docs->pluck('id')->toArray(), $tvList);
}

/**
* Reads the given TVs of many documents in one query: [docid => [tvname => value]].
*
* Only the named TVs are read, through the (tmplvarid, contentid) unique index, so a
* listing of cards, prices or any other per-document fields costs one query however
* many documents it shows. Every requested document gets every TV that exists; a TV
* without a stored (or with an empty) value falls back to its default_text unless
* $withDefaults is false, then it is ''. Values are raw: no widget rendering and no
* template assignment check, as with getTvList(). Returns [] when no TV matches.
*
* @param array $docIds document ids
* @param array $tvNames TV names, or TV ids when every element is numeric
*/
public static function getTvValues(array $docIds, array $tvNames, bool $withDefaults = true): array
{
$docIds = array_values(array_unique(array_map('intval', $docIds)));
$tvNames = array_values(array_unique(array_filter(array_map('trim', array_map('strval', $tvNames)), 'strlen')));
if ($docIds === [] || $tvNames === []) {
return [];
} else {
$ids = $docs->pluck('id')->toArray();
$tvs = SiteTmplvar::whereIn('name', $tvList)->get();
$tvNames = $tvs->pluck('default_text', 'name')->toArray();
$tvIds = $tvs->pluck('name', 'id')->toArray();
$tvValues = SiteTmplvarContentvalue::whereIn('contentid', $ids)->whereIn('tmplvarid', array_keys($tvIds))->get()->toArray();
foreach ($tvValues as $tv) {
if (empty($tv['value']) && !empty($tvNames[$tvIds [$tv['tmplvarid']]])) {
$tv['value'] = $tvNames[$tvIds[$tv['tmplvarid']]];
}
unset($tv['id']);
$docsTV[$tv['contentid']][$tv['tmplvarid']] = $tv;
}
foreach ($ids as $docid) {
foreach ($tvIds as $tvid => $tvname) {
if (empty($docsTV[$docid][$tvid])) {
$docsTV[$docid][$tvid] = ['tmplvarid' => $tvid, 'contentid' => $docid, 'value' => $tvNames[$tvIds [$tvid]]];
}
}
}
}
if (!empty($docsTV)) {
$tmp = [];
foreach ($docsTV as $docid => $tvs) {
foreach ($tvs as $tvid => $tv) {
$tmp[$docid][$tvIds[$tvid]] = $tv['value'];
}
$byId = count(array_filter($tvNames, 'is_numeric')) === count($tvNames);

$rows = (new SiteTmplvar())->getConnection()
->table('site_tmplvars as tv')
->leftJoin('site_tmplvar_contentvalues as tvc', function ($join) use ($docIds) {
$join->on('tvc.tmplvarid', '=', 'tv.id')->whereIntegerInRaw('tvc.contentid', $docIds);
})
->whereIn($byId ? 'tv.id' : 'tv.name', $byId ? array_map('intval', $tvNames) : $tvNames)
->orderBy('tv.id')
->get(['tv.name', 'tv.default_text', 'tvc.contentid', 'tvc.value']);
if ($rows->isEmpty()) {
return [];
}

$defaults = [];
$values = [];
foreach ($rows as $row) {
$defaults[$row->name] = $withDefaults ? (string) $row->default_text : '';
if ($row->contentid !== null && (string) $row->value !== '') {
$values[(int) $row->contentid][$row->name] = (string) $row->value;
}
$docsTV = $tmp;
}
return $docsTV;

$result = [];
foreach ($docIds as $docId) {
$result[$docId] = array_replace($defaults, $values[$docId] ?? []);
}

return $result;
}

//return docs array with tvs
Expand Down
102 changes: 102 additions & 0 deletions core/tests/Unit/Models/SiteContentTvValuesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

use EvolutionCMS\Models\SiteContent;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Facade;

afterEach(fn () => Facade::clearResolvedInstances());

/**
* TVs: 1 price (default "0"), 2 sku (no default), 3 unused (default "u").
* Documents 10 and 11 carry values, 12 has none, 11 stores an empty price.
* A prefixed connection, as on a real install, so the table aliases are exercised.
*/
function bootTvValuesDatabase(): Capsule
{
$capsule = new Capsule();
$capsule->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'evo_']);
$capsule->setAsGlobal();
$capsule->bootEloquent();
Model::setConnectionResolver($capsule->getDatabaseManager());

$schema = $capsule->getConnection()->getSchemaBuilder();
$schema->create('site_tmplvars', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->default('');
$table->text('default_text')->nullable();
});
$schema->create('site_tmplvar_contentvalues', function (Blueprint $table) {
$table->increments('id');
$table->integer('tmplvarid');
$table->integer('contentid');
$table->text('value')->nullable();
$table->unique(['tmplvarid', 'contentid']);
});

Capsule::table('site_tmplvars')->insert([
['id' => 1, 'name' => 'price', 'default_text' => '0'],
['id' => 2, 'name' => 'sku', 'default_text' => ''],
['id' => 3, 'name' => 'unused', 'default_text' => 'u'],
]);
Capsule::table('site_tmplvar_contentvalues')->insert([
['tmplvarid' => 1, 'contentid' => 10, 'value' => '19.90'],
['tmplvarid' => 2, 'contentid' => 10, 'value' => 'A-10'],
['tmplvarid' => 3, 'contentid' => 10, 'value' => 'not asked'],
['tmplvarid' => 1, 'contentid' => 11, 'value' => ''],
['tmplvarid' => 2, 'contentid' => 11, 'value' => 'A-11'],
['tmplvarid' => 1, 'contentid' => 99, 'value' => 'other document'],
]);

return $capsule;
}

test('reads only the named TVs of every document in one query', function () {
$capsule = bootTvValuesDatabase();
$capsule->getConnection()->enableQueryLog();

$values = SiteContent::getTvValues([10, 11, 12], ['price', 'sku']);

expect($values)->toBe([
10 => ['price' => '19.90', 'sku' => 'A-10'],
11 => ['price' => '0', 'sku' => 'A-11'],
12 => ['price' => '0', 'sku' => ''],
])->and($capsule->getConnection()->getQueryLog())->toHaveCount(1);
});

test('without defaults a missing or empty value is an empty string', function () {
bootTvValuesDatabase();

expect(SiteContent::getTvValues([11, 12], ['price'], false))->toBe([
11 => ['price' => ''],
12 => ['price' => ''],
]);
});

test('accepts TV ids and string document ids', function () {
bootTvValuesDatabase();

expect(SiteContent::getTvValues(['10', 10], [1, '2']))->toBe([
10 => ['price' => '19.90', 'sku' => 'A-10'],
]);
});

test('returns nothing without documents, without names or when no TV matches', function () {
bootTvValuesDatabase();

expect(SiteContent::getTvValues([], ['price']))->toBe([])
->and(SiteContent::getTvValues([10], ['', ' ']))->toBe([])
->and(SiteContent::getTvValues([10], ['missing']))->toBe([]);
});

test('getTvList keeps its result shape on top of the bulk read', function () {
bootTvValuesDatabase();
$docs = new Collection([['id' => 10], ['id' => 12]]);

expect(SiteContent::getTvList($docs, ['price']))->toBe([
10 => ['price' => '19.90'],
12 => ['price' => '0'],
])->and(SiteContent::getTvList($docs, []))->toBe([]);
});
Loading