Я обычно выгребаю всю вложенность одной портянкой (добиваемся уникальности имен полей, добавляя необходимые префиксы):
Select p.id as post_id, p.create_date as p_create_date, p.title as p_title, p.text as p_text,
img.id as img_id, img.name as img_name,
v.id as v_id, v.name as v_name
from posts p
left join images img on img.post_id = p.id
left join videos v on v.post_id = p.id
А потом в PHP собираю ассоциативный массив с нужной глубиной вложенности:
$out = [];
foreach($rows as $row)
{
$post = &$out[$row['POST_ID']]; //ссылка на элемент массива первого уровня - пост
$post['CREATE_DATE'] = $row['P_CREATE_DATE'];
$post['TITLE'] = $row['P_TITLE'];
$post['TEXT'] = $row['P_TEXT'];
if(!is_null($row['IMG_ID']))
{
$image = &$post['IMAGES'][$row['IMG_ID']]; //ссылка на элемент массива второго уровня - элемент массива изображений
$image['NAME'] = $row['IMG_NAME'];
}
if(!is_null($row['V_ID']))
{
$video = &$post['VIDEOS'][$row['V_ID']]; //ссылка на элемент массива второго уровня - элемент массива видео
$video['NAME'] = $row['V_NAME'];
}
}
На выходе получаем $out - вполне себе структуированный объект, пригодный для дальнейшей обработки и выдачи на фронтенд.
PS: Использование ссылок ускоряет сборку таких структур.
Например, чтобы заполнить несколько свойств элемента массива IMAGES, который вложен, быстрее будет отрабатывать конструкция:
$post = &$out[$row['POST_ID']];
$image = &$post['IMAGES'][$row['IMG_ID']];
$image['NAME'] = $row['IMG_NAME'];
$image['WIDTH'] = $row['IMG_WIDTH'];
$image['HEIGHT'] = $row['IMG_HEIGHT'];
Чем вот такое нагромождение для заполнения каждого свойства:
$out[$row['POST_ID']]['IMAGES'][$row['IMG_ID']]['NAME'] = $row['IMG_NAME'];
$out[$row['POST_ID']]['IMAGES'][$row['IMG_ID']]['WIDTH'] = $row['IMG_WIDTH'];
$out[$row['POST_ID']]['IMAGES'][$row['IMG_ID']]['HEIGHT'] = $row['IMG_HEIGHT'];