Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
97d04c5
feat: fix N+1 problem
smarcet Feb 20, 2026
e4d015c
feat: fix N+1 problem
smarcet Feb 20, 2026
fd1871e
fix(serializer): restore allowed_access_levels key in PresentationCat…
smarcet May 23, 2026
483fb0e
feat(repository): extend DoctrineRepository::getAllByPage with N+1 ba…
smarcet May 23, 2026
fd8eaa4
chore(repository): replace time() with microtime(true) and add per-ph…
smarcet May 23, 2026
4857e00
fix(repository): remove redundant et2 join from hydration query in ge…
smarcet May 23, 2026
d655630
fix(serializer): eliminate speaker lazy-loads and enable presentation…
smarcet May 23, 2026
302a28d
fix(serializer): add last_edited timestamp to presentation cache key …
smarcet May 23, 2026
967d737
chore(debug): add serializer timing logs to getEvents and PagingRespo…
smarcet May 23, 2026
ca2ca3b
fix(serializer): force-initialize EXTRA_LAZY collections and pre-load…
smarcet May 23, 2026
72b3063
fix(graph-loader): use em->find() instead of tryGetById for EXTRA_LAZ…
smarcet May 23, 2026
a447c28
fix(graph-loader): correct tryGetById call for EXTRA_LAZY collection …
smarcet May 23, 2026
81615e1
fix(serializer): pre-load PresentationSpeaker + Member in single DQL …
smarcet May 23, 2026
b3b9eb2
feat(serializer): add Redis cache to SummitEventSerializer
smarcet May 23, 2026
4b009bd
fix(serializer): include media_uploads serializer type in cache key, …
smarcet May 23, 2026
54af823
chore(debug): remove per-item and per-phase diagnostic logging
smarcet May 23, 2026
d3bed78
fix(serializer): collapse Cache::has+Cache::get into single Cache::ge…
smarcet May 23, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,19 @@ public function getEvents($summit_id)
$current_user = $this->resource_server_context->getCurrentUser(true);
return $this->withReplica(function() use ($summit_id, $current_user) {
$strategy = new RetrieveAllSummitEventsBySummitStrategy($this->repository, $this->event_repository, $this->resource_server_context);
$response = $strategy->getEvents(['summit_id' => $summit_id]);
$expand = SerializerUtils::getExpand();
$response = $strategy->getEvents(['summit_id' => $summit_id, 'expand' => $expand]);
return $this->ok
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[
'current_user' => $current_user
],
'current_user' => $current_user,
'use_cache' => true,
],
$this->getSerializerType()
)
);
Expand Down Expand Up @@ -234,12 +236,13 @@ public function getEventsCSV($summit_id)
$this->event_repository,
$this->resource_server_context
);
$response = $strategy->getEvents(['summit_id' => $summit_id]);
$expand = SerializerUtils::getExpand();
$response = $strategy->getEvents(['summit_id' => $summit_id, 'expand' => $expand]);

$filename = "activities-" . date('Ymd');
$list = $response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
['none'],
[
Expand Down Expand Up @@ -328,18 +331,20 @@ public function getScheduledEvents($summit_id)
$summit = SummitFinderStrategyFactory::build($this->getRepository(), $this->getResourceServerContext())->find($summit_id);
if (is_null($summit)) return $this->error404();

$expand = SerializerUtils::getExpand();
$params = [
'summit_id' => $summit_id,
'summit' => $summit,
'published' => true,
'current_user' => $current_user
'current_user' => $current_user,
'expand' => $expand,
];

$strategy = new RetrievePublishedSummitEventsBySummitStrategy($this->repository, $this->event_repository, $this->resource_server_context);
$response = $strategy->getEvents($params);
return $this->ok($response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
$params,
Expand Down Expand Up @@ -430,13 +435,14 @@ public function getAllEvents()
$current_user = $this->resource_server_context->getCurrentUser(true);

return $this->withReplica(function() use($current_user){
$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllSummitEventsStrategy($this->event_repository);
$response = $strategy->getEvents();
$response = $strategy->getEvents(['expand' => $expand]);
return $this->ok
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[
Expand Down Expand Up @@ -482,16 +488,17 @@ public function getAllPresentations($summit_id)
$current_user = $this->resource_server_context->getCurrentUser(true);

return $this->withReplica(function() use($current_user, $summit_id){
$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllSummitPresentationsStrategy($this->repository, $this->event_repository, $this->resource_server_context);
$response = $strategy->getEvents(['summit_id' => intval($summit_id)]);
$response = $strategy->getEvents(['summit_id' => intval($summit_id), 'expand' => $expand]);
$params = [
'current_user' => $current_user,
];
return $this->ok
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
$params,
Expand Down Expand Up @@ -536,14 +543,15 @@ public function getAllVoteablePresentations($summit_id)
$summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id);
if (is_null($summit)) throw new EntityNotFoundException;

$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllSummitVoteablePresentationsStrategy
(
$this->repository,
$this->event_repository,
$this->resource_server_context
);

$response = $strategy->getEvents(['summit_id' => intval($summit_id)]);
$response = $strategy->getEvents(['summit_id' => intval($summit_id), 'expand' => $expand]);

$params = [
'current_user' => $this->resource_server_context->getCurrentUser(true),
Expand All @@ -554,7 +562,7 @@ public function getAllVoteablePresentations($summit_id)
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
$params,
Expand Down Expand Up @@ -599,13 +607,14 @@ public function getAllVoteablePresentationsV2($summit_id)
$summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id);
if (is_null($summit)) throw new EntityNotFoundException;

$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllSummitVoteablePresentationsStrategy
(
$this->repository,
$this->event_repository,
$this->resource_server_context
);
$response = $strategy->getEvents(['summit_id' => intval($summit_id)]);
$response = $strategy->getEvents(['summit_id' => intval($summit_id), 'expand' => $expand]);

$params = [
'current_user' => $this->resource_server_context->getCurrentUser(true),
Expand All @@ -628,7 +637,7 @@ public function getAllVoteablePresentationsV2($summit_id)
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
$params,
Expand Down Expand Up @@ -667,14 +676,15 @@ public function getAllVoteablePresentationsV2CSV($summit_id)
$summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id);
if (is_null($summit)) throw new EntityNotFoundException;

$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllSummitVoteablePresentationsStrategyCSV
(
$this->repository,
$this->event_repository,
$this->resource_server_context
);

$response = $strategy->getEvents(['summit_id' => intval($summit_id)]);
$response = $strategy->getEvents(['summit_id' => intval($summit_id), 'expand' => $expand]);

$params = [
'current_user' => $this->resource_server_context->getCurrentUser(true),
Expand All @@ -697,7 +707,7 @@ public function getAllVoteablePresentationsV2CSV($summit_id)
$filename = "voteable-presentations-" . date('Ymd');
$list = $response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
['none'],
$params,
Expand Down Expand Up @@ -784,13 +794,14 @@ public function getAllScheduledEvents()
$current_user = $this->resource_server_context->getCurrentUser(true);

return $this->withReplica(function () use ($current_user) {
$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllPublishedSummitEventsStrategy($this->event_repository);
$response = $strategy->getEvents();
$response = $strategy->getEvents(['expand' => $expand]);
return $this->ok
(
$response->toArray
(
SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[
Expand Down Expand Up @@ -1970,13 +1981,14 @@ public function getUnpublishedEvents($summit_id)
}

return $this->withReplica(function() use($summit_id, $serializer_type){
$expand = SerializerUtils::getExpand();
$strategy = new RetrieveAllUnPublishedSummitEventsStrategy($this->repository, $this->event_repository, $this->resource_server_context);

$response = $strategy->getEvents(['summit_id' => $summit_id]);
$response = $strategy->getEvents(['summit_id' => $summit_id, 'expand' => $expand]);
return $this->ok($response->toArray
(

SerializerUtils::getExpand(),
$expand,
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ protected function buildFilter(){
* @param Order|null $order
* @return PagingResponse
*/
public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null): PagingResponse
public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null, array $expands = []): PagingResponse
{
return $this->events_repository->getAllByPage($paging_info, $filter, $order);
return $this->events_repository->getAllByPage($paging_info, $filter, $order, $expands);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ public function __construct(ISummitEventRepository $event_repository)
* @param Order|null $order
* @return PagingResponse
*/
public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null)
public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null, array $expands = [])
{
return $this->event_repository->getAllByPage($paging_info, $filter, $order);
return $this->event_repository->getAllByPage($paging_info, $filter, $order, $expands);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ public function getEvents(array $params = [])

list($page, $per_page) = $this->getPaginationParams();

// Parse expand parameter into array for repository batch-loading
$expandStr = $params['expand'] ?? '';
$expands = !empty($expandStr) ? array_map('trim', explode(',', $expandStr)) : [];

return $this->retrieveEventsFromSource
(
new PagingInfo($page, $per_page), $this->buildFilter(), $this->buildOrder()
new PagingInfo($page, $per_page), $this->buildFilter(), $this->buildOrder(), $expands
);
}

Expand Down Expand Up @@ -152,7 +156,7 @@ protected function buildOrder()
* @param Order|null $order
* @return PagingResponse
*/
abstract public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null);
abstract public function retrieveEventsFromSource(PagingInfo $paging_info, Filter $filter = null, Order $order = null, array $expands = []);

/**
* @return array
Expand Down
42 changes: 13 additions & 29 deletions app/ModelSerializers/Summit/Presentation/PresentationSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,47 +104,31 @@ public function serialize($expand = null, array $fields = [], array $relations =
$presentation = $this->object;
if(!$presentation instanceof Presentation) return [];

// Include last_edited timestamp so a presentation update naturally busts the cache
// without needing an explicit Cache::forget — the old key just ages out via TTL.
// Include media_uploads serializer type so different user roles (Public/Admin) get
// their own cache entry — this lets us cache the full serialized media_uploads array
// and skip per-cache-hit re-serialization, which was the dominant cost on the cache-hit
// path for presentations with many media uploads.
$key =
sprintf
(
"public_presentation_%s_%s_%s_%s",
"public_presentation_%s_%s_%s_%s_%s_%s",
$presentation->getId(),
$presentation->getLastEditedUTC()?->getTimestamp() ?? 0,
$this->getMediaUploadsSerializerType(),
$expand ?? "",
implode(",",$fields),
implode(",", $relations)
);

$use_cache = $params['use_cache'] ?? false;

if($use_cache && Cache::has($key)){
$values = json_decode(Cache::get($key), true);
Log::debug(sprintf("PresentationSerializer::serialize cache hit for presentation %s", $presentation->getId()));
if (!empty($expand)) {
foreach (explode(',', $expand) as $relation) {
$relation = trim($relation);
switch ($relation) {
case 'media_uploads':
{
$media_uploads = [];

foreach ($presentation->getMediaUploads() as $mediaUpload) {
$media_uploads[] = SerializerRegistry::getInstance()->getSerializer
(
$mediaUpload, $this->getMediaUploadsSerializerType()
)->serialize
(
AbstractSerializer::filterExpandByPrefix($expand, $relation),
AbstractSerializer::filterFieldsByPrefix($fields, $relation),
AbstractSerializer::filterFieldsByPrefix($relations, $relation),
);
}

$values['media_uploads'] = $media_uploads;
}
}
}
if($use_cache){
$cached = Cache::get($key);
if ($cached !== null) {
return json_decode($cached, true);
}
return $values;
}

$values = parent::serialize($expand, $fields, $relations, $params);
Expand Down
32 changes: 32 additions & 0 deletions app/ModelSerializers/Summit/SummitEventSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* limitations under the License.
**/

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Libs\ModelSerializers\AbstractSerializer;
use models\summit\SummitEvent;

Expand All @@ -21,6 +23,8 @@
*/
class SummitEventSerializer extends SilverStripeSerializer
{
const CacheTTL = 1200;


protected static $array_mappings = [
'Title' => 'title:json_string',
Expand Down Expand Up @@ -136,6 +140,30 @@ public function serialize
$event = $this->object;
if (!$event instanceof SummitEvent) return [];

// Cache opt-in via params. Skipped for PresentationSerializer (and its
// subclasses) because PresentationSerializer has its own, more specific
// cache at the presentation level. static::class is included in the key
// so each SummitEvent subclass (SummitGroupEventSerializer, ...) caches
// independently and doesn't collide with siblings.
$use_cache = ($params['use_cache'] ?? false)
&& !($this instanceof PresentationSerializer);
$cache_key = null;
if ($use_cache) {
$cache_key = sprintf(
"public_summit_event_%s_%s_%s_%s_%s_%s",
static::class,
$event->getId(),
$event->getLastEditedUTC()?->getTimestamp() ?? 0,
$expand ?? "",
implode(",", $fields),
implode(",", $relations)
);
$cached = Cache::get($cache_key);
if ($cached !== null) {
return json_decode($cached, true);
}
}

$values = parent::serialize($expand, $fields, $relations, $params);

if (in_array('sponsors', $relations))
Expand Down Expand Up @@ -337,6 +365,10 @@ public function serialize
}
}

if ($use_cache && $cache_key !== null) {
Cache::put($cache_key, json_encode($values), self::CacheTTL);
}

return $values;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function getByFullName(string $fullname):?Member;
* @param Order|null $order
* @return PagingResponse
*/
public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null);
public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null, array $expands = []);


/**
Expand Down
Loading
Loading