<?php
namespace Cms\FrontendBundle\Service\Resolvers;
use App\Doctrine\Repository\System\SchoolRepository;
use App\Entity\System\School;
use Cms\ContainerBundle\Entity\Container;
use Cms\ContainerBundle\Entity\Containers\GenericContainer;
use Cms\ContainerBundle\Entity\Containers\IntranetContainer;
use Cms\ContainerBundle\Entity\Containers\PersonalContainer;
use Cms\TenantBundle\Entity\Tenant;
use Cms\TenantBundle\Model\SimpleTenantableInterface;
use Cms\TenantBundle\Model\TenantableInterface;
use Products\NotificationsBundle\Entity\AbstractList;
use Products\NotificationsBundle\Entity\Lists\ConditionList;
use Products\NotificationsBundle\Entity\Lists\DistrictList;
use Products\NotificationsBundle\Entity\Lists\SchoolList;
use Products\NotificationsBundle\Entity\Student;
/**
*
*/
final class SchoolResolver extends AbstractResolver
{
/**
* @param TenantableInterface $tenantable
* @return School|null
*/
public function resolveDistrictByTenant(SimpleTenantableInterface $tenantable): ?School
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $tenantable->getTenant()->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// set in cache and return the value
return $this->cache(
$lookup,
$this->em->getRepository(School::class)->findOneBy([
'tenant' => $tenantable->getTenant(),
'type' => School::TYPES__DISTRICT,
])
);
}
/**
* @param SimpleTenantableInterface $tenantable
* @return array|School[]
*/
public function resolveSchoolsByTenant(SimpleTenantableInterface $tenantable): array
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $tenantable->getTenant()->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// find all the schools
$schools = $this->em->getRepository(School::class)->findAll();
// set in cache and return the value
return $this->cache($lookup, $schools);
}
/**
* @param int $type
* @return array|School[]
*/
public function resolveSchoolsByType(int $type): array
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $type);
if ( ! is_string($lookup)) {
return $lookup;
}
// find schools by type
$schools = $this->em->getRepository(School::class)->findBy(
[
'type' => $type,
],
[
'name' => 'ASC',
]
);
return $this->cache($lookup, $schools);
}
/**
* @param SimpleTenantableInterface $tenantable
* @param int $types
* @return array|School[]
*/
public function resolveSchoolsByTypes(SimpleTenantableInterface $tenantable, int $types): array
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $types);
if ( ! is_string($lookup)) {
return $lookup;
}
/** @var SchoolRepository $schoolRepository */
$schoolRepository = $this->em->getRepository(School::class);
$schools = $schoolRepository->findAllForTenantAndSchoolTypes(
$tenantable->getTenant(),
$types,
);
return $this->cache($lookup, $schools);
}
/**
* @param Container $department
* @return School|null
*/
public function resolveSchoolByDepartment(Container $department): ?School
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $department->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// branch on the type
switch (true) {
// passthrough to specific handlers
case $department instanceof GenericContainer:
$school = $this->resolveSchoolByPublicDepartment($department);
break;
case $department instanceof IntranetContainer:
$school = $this->resolveSchoolByIntranetDepartment($department);
break;
case $department instanceof PersonalContainer:
$school = $this->resolveSchoolByPrivateDepartment($department);
break;
// unsupported
default:
throw new \LogicException();
}
// set in cache and return the value
return $this->cache($lookup, $school);
}
/**
* @param GenericContainer $department
* @return School|null
*/
protected function resolveSchoolByPublicDepartment(GenericContainer $department): ?School
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $department->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// get all the schools
// TODO: prevent this by attaching school to department objects as well as departments to schools
$schools = $this->resolveSchoolsByTenant($department);
// if not found we need to pull our ancestors, the closest first
$ancestors = $this->rm->getDepartmentResolver()->resolveNearestAncestorsWithDepartment($department);
// start looping over them to try and find a school
$found = null;
foreach ($ancestors as $ancestor) {
foreach ($schools as $school) {
if ($school->getDepartment() && $school->getDepartment()->getId() === $ancestor->getId()) {
$found = $school;
break 2;
}
}
}
// set in cache and return the value
return $this->cache($lookup, $found);
}
/**
* @param IntranetContainer $department
* @return School|null
*/
protected function resolveSchoolByIntranetDepartment(IntranetContainer $department): ?School
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $department->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// TODO: currently no school support for intranets?
// set in cache and return the value
return $this->cache($lookup, null);
}
/**
* @param PersonalContainer $department
* @return School|null
*/
protected function resolveSchoolByPrivateDepartment(PersonalContainer $department): ?School
{
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $department->getId());
if ( ! is_string($lookup)) {
return $lookup;
}
// TODO
// set in cache and return the value
return $this->cache($lookup, null);
}
/**
* When given a set of sourcedIds, will return an array of information for those IDs.
* The function will attempt to pull all IDs it can from the resolver cache first.
* Then, any remaining IDs that were not in cache in any way (NULL or a School entity) will be looked for in the database.
* This will return an associative array with keys of the "sourcedIds" and values of what was found (can be NULL or a School).
* If a cleaner array result is needed, the calling code will need to utilize array functions to clean up the result.
* (If this becomes a fairly common practice, a helper function in this class could be introduced to perform that logic.)
*
* TODO: 2024-06-25
* Do we want to keep NULLs if the sourcedId lookup doesn't return anything?
*
* @param array|string[] $sourcedIds
* @return array|School[]
*/
public function resolveSchoolsBySourcedIds(array $sourcedIds): array
{
// holder for ids we have to grab from the database still
// this method optimizes the resolver cache, so if we don't need to find an entity in the db, we don't query for it
$oneRosterOrgs = [];
// generate the lookups
// a string value in the array is a cache key meaning the entity has not been loaded yet
// a non-string value means the sourcedid has been looked up prior in the db and the result was cached
$lookups = [];
foreach ($sourcedIds as $sourcedId) {
// attempts to find whether the sourcedId has already been looked for in the db
// uses the function name of the single-sourcedId method so that we can piggyback off anything already cached
$lookups[$sourcedId] = $lookup = $this->attempt('resolveSchoolBySourcedId', $sourcedId);
// if a string, that means we need to try to find this in the db still
if (is_string($lookup)) {
$oneRosterOrgs[] = $sourcedId;
}
}
// find the rest of the schools
// do only if we have ids to try and find
if ($oneRosterOrgs) {
// run the database query
// note that some sourcedIds may not be found
$schools = $this->em->getRepository(School::class)->findBy([
'oneRosterOrg' => $oneRosterOrgs,
]);
// loop over the results
foreach ($schools as $school) {
// first, we need to add what we found to the cache for future use
$this->cache($lookups[$school->getOneRosterOrg()], $school);
// then we need to fix our local copy of results
$lookups[$school->getOneRosterOrg()] = $school;
}
}
// need to run through the array one more time
// any values that are still strings need to be nulled as they were not found
// this means caching nulls so a future attempt to find the same sourcedId does not result in another db query
foreach ($lookups as $key => $value) {
if (is_string($value)) {
$this->cache($value, null);
$lookups[$key] = null;
}
}
// note that this will return nulls in the final array
// use array functions to clean up the result if needed
return $lookups;
}
/**
* Attempts to find a single School based on it's One Roster "org" association based on the "sourcedId".
*
* @param string|null $sourcedId
* @return School|null
*/
public function resolveSchoolBySourcedId(?string $sourcedId): ?School
{
// allow for nulls for dx
if ( ! ($sourcedId ?: null)) {
return null;
}
// generate the lookup
$lookup = $this->attempt(__FUNCTION__, $sourcedId);
if ( ! is_string($lookup)) {
return $lookup;
}
// find school by the id
$school = $this->em->getRepository(School::class)->findOneBy([
'oneRosterOrg' => $sourcedId,
]);
// set in cache and return the value
return $this->cache($lookup, $school);
}
/**
* @param Student[] $students
* @return School[]
*/
public function resolveSchoolsByStudents(array $students): array
{
$primarySchoolSourceIds = array_unique(
array_filter(
array_map(static function (Student $student) {
return $student->getMetadataPrimarySchool();
}, $students)
)
);
return $this->resolveSchoolsBySourcedIds($primarySchoolSourceIds);
}
/**
* @param AbstractList[] $lists
* @return School[]
*/
public function resolveSchoolsByLists(array $lists): array
{
$schoolSourceIds = array_unique(
array_filter(
array_map(static function (AbstractList $list) {
switch (true) {
case $list instanceof DistrictList || $list instanceof SchoolList:
return $list->getOneRosterId();
case $list instanceof ConditionList:
return $list->getSchool() ? $list->getSchool()->getOneRosterOrg() : null;
default:
return null;
}
}, $lists)
)
);
return $this->resolveSchoolsBySourcedIds($schoolSourceIds);
}
}