<?php
namespace Products\NotificationsBundle\Subscriber\OneRoster;
use App\Service\Data\PhoneNumberService;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Entity\OneRosterJob;
use Cms\CoreBundle\Entity\OneRosterSync;
use Cms\CoreBundle\Events\OneRosterProcessEvent;
use Cms\CoreBundle\Service\OneRosterService;
use Cms\CoreBundle\Util\DateTimeUtils;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use DateTime;
use Doctrine\Common\Util\ClassUtils;
use Products\NotificationsBundle\Entity\AbstractRecipient;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Entity\ProfileContact;
use Products\NotificationsBundle\Entity\Recipients\EmailRecipient;
use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
use Products\NotificationsBundle\Entity\Student;
use Products\NotificationsBundle\Util\Preferences;
use Products\NotificationsBundle\Util\Reachability;
use Symfony\Contracts\Translation\TranslatorInterface;
final class OneRosterUserProcessSubscriber extends AbstractNotificationsOneRosterSubscriber
{
// DI
protected PhoneNumberService $phones;
/**
* {@inheritdoc}
* @param PhoneNumberService $phones
*/
public function __construct(
EntityManager $em,
OneRosterService $oneroster,
TranslatorInterface $translator,
PhoneNumberService $phones
)
{
parent::__construct($em, $oneroster, $translator);
$this->phones = $phones;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
OneRosterProcessEvent::EVENT__USER => [
['syncProfile', 0],
['syncStudent', 0],
['syncRecipients', 0],
['initPreferences', 0],
['calculateReachability', 0],
],
];
}
/**
* @param OneRosterProcessEvent $event
*/
public function syncProfile(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getJob(), [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// branch on the role type
switch (true) {
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) && $user->isRoleStaff():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) && $user->isRoleFamily():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) && $user->isRoleStudent():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) && $user->isRoleCommunity():
// noop, code will continue after the switch statement
break;
default:
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// return to prevent further processing
return;
}
// attempt to find a profile, first by oneroster id
$profile = $this->em->getRepository(Profile::class)->findOneBy([
'onerosterId' => $user->getSourcedId(),
]);
// IMPORTANT: if we don't find by oneroster id, we cannot look up any other way as it is not safe/private
// generate a new profile if we don't have one
if ( ! $profile) {
$profile = new Profile();
}
// determine the usability of the user
// if we are to ignore the enabledUser property (different schools have different takes on this), then we just use the active status
// otherwise, we need to incorporate that enabledUser property into our calculations
$usable = $event->getSync()->hasFlag(OneRosterSync::FLAGS__IGNORE_ENABLED_USER_PROPERTY)
? $user->isStatusActive()
: $user->isUsable();
// clear discarded just in case it was previously set
// do only if the user is active according to oneroster spec
// else, use whatever status was currently set on the
$profile->setDiscardedAt(
$usable
? null
: ($profile->getDiscardedAt() ?? new DateTime())
);
// set stuff
$profile
->setFirstName($user->getGivenName())
->setLastName($user->getFamilyName())
->setRole($user->getRole())
->setOneRosterId($user->getSourcedId())
;
// compile orgs
$orgs = array_values(array_unique(array_merge(
$user->hasMetadataEntry('gg4l.primary_school')
? [$user->getMetadataEntry('gg4l.primary_school')]
: [],
$user->getOrgsSourcedIds(),
)));
if ($event->getSync()->hasFlag(OneRosterSync::FLAGS__SINGLE_SCHOOL)) {
$orgs = [$event->getSync()->getDistrictId()];
}
// handle core metadata
$metadata = [
'_role' => $user->getRole(),
'_role_type' => OneRosterUser::TYPES_LOOKUP[
OneRosterUser::ROLES_MAPPING[$user->getRole()]
],
'_orgs' => $orgs,
'_schools' => array_values(array_diff(
$orgs,
[$event->getSync()->getDistrictId()],
)),
'_district' => $event->getSync()->getDistrictId(),
'_grades' => array_values(array_unique(array_merge(
$user->hasMetadataEntry('gg4l.primaryGrade')
? [$user->getMetadata()['gg4l.primaryGrade']]
: [],
$user->getGrades(),
))),
];
// handle metadata
$profile->setMetadata(array_merge(
$user->getMetadata() ?: [],
$metadata,
));
// handle language setting if passed with metadata
// this needs done after the metadata has been set as it could change metadata
try {
$profile->setLanguage(
$user->hasMetadataEntry(Profile::METADATA__LANGUAGE)
? $user->getMetadataEntry(Profile::METADATA__LANGUAGE)
: null
);
} catch (\Exception $e) {
$profile->setLanguage(null);
}
// track archived status
$profile->setOneRosterArchived($user->isStatusToBeDeleted());
// DEBUGGING
$event->getOutput()->writeln(sprintf(
' %s %s (%s | %s | %s)',
(empty($profile->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($profile),
$profile->getLastName(),
$profile->getOneRosterId(),
$profile->getId() ?: '-'
));
// save it
$this->em->save($profile);
}
/**
* @param OneRosterProcessEvent $event
*/
public function syncStudent(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getJob(), [
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// branch on the role type
switch (true) {
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) && $user->isRoleStudent():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) && $user->isRoleStudent():
// noop, code will continue after the switch statement
break;
default:
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// return to prevent further processing
return;
}
// attempt to find a student, first by oneroster id
$student = $this->em->getRepository(Student::class)->findOneBy([
'onerosterId' => $user->getSourcedId(),
]);
// IMPORTANT: if we don't find by oneroster id, we cannot look up any other way as it is not safe/private
// generate a new profile if we don't have one
if ( ! $student) {
$student = new Student();
}
// determine the usability of the user
// if we are to ignore the enabledUser property (different schools have different takes on this), then we just use the active status
// otherwise, we need to incorporate that enabledUser property into our calculations
$usable = $event->getSync()->hasFlag(OneRosterSync::FLAGS__IGNORE_ENABLED_USER_PROPERTY)
? $user->isStatusActive()
: $user->isUsable();
// clear discarded just in case it was previously set
// do only if the user is active according to oneroster spec
// else, use whatever status was currently set on the
$student->setDiscardedAt(
$usable
? null
: ($student->getDiscardedAt() ?? new DateTime())
);
// set stuff
$student
->setFirstName($user->getGivenName())
->setLastName($user->getFamilyName())
->setOneRosterId($user->getSourcedId())
;
// compile orgs
$orgs = array_values(array_unique(array_merge(
$user->hasMetadataEntry('gg4l.primary_school')
? [$user->getMetadataEntry('gg4l.primary_school')]
: [],
$user->getOrgsSourcedIds(),
)));
if ($event->getSync()->hasFlag(OneRosterSync::FLAGS__SINGLE_SCHOOL)) {
$orgs = [$event->getSync()->getDistrictId()];
}
// handle core metadata
$metadata = [
'_role' => $user->getRole(),
'_role_type' => OneRosterUser::TYPES_LOOKUP[
OneRosterUser::ROLES_MAPPING[$user->getRole()]
],
'_orgs' => $orgs,
'_schools' => array_values(array_diff(
$orgs,
[$event->getSync()->getDistrictId()],
)),
'_district' => $event->getSync()->getDistrictId(),
'_grades' => array_values(array_unique(array_merge(
$user->hasMetadataEntry('gg4l.primaryGrade')
? [$user->getMetadata()['gg4l.primaryGrade']]
: [],
$user->getGrades(),
))),
];
// handle metadata
$student->setMetadata(array_merge(
$user->getMetadata() ?: [],
$metadata
));
// track archived status
$student->setOneRosterArchived($user->isStatusToBeDeleted());
// DEBUGGING
$event->getOutput()->writeln(sprintf(
' %s %s (%s | %s | %s)',
(empty($student->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($student),
$student->getCensoredName(),
$student->getOneRosterId(),
$student->getId() ?: '-'
));
// save it
$this->em->save($student);
}
/**
* @param OneRosterProcessEvent $event
*/
public function syncRecipients(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getJob(), [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// branch on the role type
switch (true) {
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) && $user->isRoleStaff():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) && $user->isRoleFamily():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) && $user->isRoleStudent():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) && $user->isRoleCommunity():
// noop, code will continue after the switch statement
break;
default:
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// quit early
return;
}
// load up the profile, this must match something, and it should at this point...
$profile = $this->em->getRepository(Profile::class)->findOneBy([
'onerosterId' => $user->getSourcedId(),
]);
// if we don't have a profile we have a problem
if ( ! $profile) {
throw new \RuntimeException(sprintf(
'Could not load profile "%s".',
$user->getSourcedId()
));
}
// assemble all the pieces of contact information we can find
$values = [];
// attempt to pull the standard email field
if ($user->getEmail()) {
$values[strtolower($user->getEmail())] = AbstractRecipient::KINDS__EMAIL;
}
// handle extra emails coming from our own data tool
if ($user->hasMetadataEntry(OneRosterPrepareSubscriber::METADATA__EXTRA_EMAILS)) {
foreach ($user->getMetadataEntry(OneRosterPrepareSubscriber::METADATA__EXTRA_EMAILS) as $email) {
$values[strtolower($email)] = AbstractRecipient::KINDS__EMAIL;
}
}
// attempt to pull the standard phone field
if ($user->getPhone()) {
try {
$val = $this->phones->normalize(
$user->getPhone()
);
if ($val) {
$values[$val] = AbstractRecipient::KINDS__VOICE;
}
} catch (\Exception $e) {
$this->oneroster->logIssue(
$event->getJob(),
OneRosterJob::PHASES__PROCESS,
OneRosterProcessEvent::EVENTS[ClassUtils::getClass($user)],
$user::ONEROSTER_TYPE,
$user,
$e
);
}
}
// attempt to pull the work phone stuff
// assume voice as work phones tend to be landlines?
foreach (OneRosterPrepareSubscriber::METADATA__PHONES as $field => $kind) {
if ($user->hasMetadataEntry($field)) {
try {
$val = $this->phones->normalize(
$user->getMetadataEntry($field)
);
if ($val) {
$values[$val] = $kind;
}
} catch (\Exception $e) {
$this->oneroster->logIssue(
$event->getJob(),
OneRosterJob::PHASES__PROCESS,
OneRosterProcessEvent::EVENTS[ClassUtils::getClass($user)],
$user::ONEROSTER_TYPE,
$user,
$e
);
}
}
}
// handle extra phones coming from our own data tool
if ($user->hasMetadataEntry(OneRosterPrepareSubscriber::METADATA__EXTRA_PHONES)) {
foreach ($user->getMetadataEntry(OneRosterPrepareSubscriber::METADATA__EXTRA_PHONES) as $phone) {
try {
$val = $this->phones->normalize($phone);
if ($val) {
$values[$val] = AbstractRecipient::KINDS__VOICE;
}
} catch (\Exception $e) {
$this->oneroster->logIssue(
$event->getJob(),
OneRosterJob::PHASES__PROCESS,
OneRosterProcessEvent::EVENTS[ClassUtils::getClass($user)],
$user::ONEROSTER_TYPE,
$user,
$e
);
}
}
}
// attempt to pull the standard sms field
// NOTE: do this after phone in case the same number is used as voice, so we can prefer sms
if ($user->getSms()) {
try {
$val = $this->phones->normalize(
$user->getSms()
);
if ($val) {
$values[$val] = AbstractRecipient::KINDS__SMS;
}
} catch (\Exception $e) {
$this->oneroster->logIssue(
$event->getJob(),
OneRosterJob::PHASES__PROCESS,
OneRosterProcessEvent::EVENTS[ClassUtils::getClass($user)],
$user::ONEROSTER_TYPE,
$user,
$e
);
}
}
// now that all the contacts are sure to be in the database, pull them
// must pull them by their value
$contacts = $this->em->getRepository(AbstractRecipient::class)->findBy([
'contact' => array_keys($values),
]);
if (count($contacts) !== count($values)) {
throw new \RuntimeException(sprintf(
'Could not load all contact information, missing: "%s".',
implode('", "', array_diff(
array_map(
static function (AbstractRecipient $contact) {
return $contact->getContact();
},
$contacts
),
array_keys($values)
))
));
}
// get all the existing contacts in our system for this person
/** @var array|ProfileContact[] $xrefs */
$xrefs = $this->em->getRepository(ProfileContact::class)->findBy([
'profile' => $profile,
]);
// track which items need to be kept in the database
$managed = [];
// need to loop over the values as those are the things we have to track
foreach ($values as $value => $kind) {
// if no value, we skip
if ( ! $value) {
continue;
}
// match to a contact
$contact = null;
foreach ($contacts as $thing) {
// TODO: should we do a more flexible string comparison?
if ($thing->getContact() === $value) {
$contact = $thing;
break;
}
}
if ( ! $contact) {
throw new \RuntimeException(sprintf(
'Could not find contact match for value "%s".',
$value
));
}
// try to find a xref
// loop over all the existing xrefs and attempt to find one that matches
$xref = null;
foreach ($xrefs as $thing) {
if ($thing->getRecipient() === $contact) {
$xref = $thing;
break;
}
}
// if none was found, we need to make one
if ( ! $xref) {
// generate a xref based on type/kind
$xref = new ProfileContact();
// we made a new one, need to also attach it to the xrefs
$xrefs[] = $xref;
}
// attach the contact to the managed entities
if ( ! array_search($xref, $managed, true)) {
$managed[] = $xref;
}
// set data
$xref
->setProfile($profile)
->setRecipient($contact)
->setOneRosterId($user->getSourcedId())
;
// handle discarded
$xref->setDiscardedAt(
$user->isStatusActive()
? null
: $xref->getDiscardedAt()
);
// determine if archived or not
$xref->setOneRosterArchived($user->isStatusToBeDeleted());
}
// DEBUGGING
array_walk($managed, static function (?ProfileContact $xref) use ($event) {
if ($xref) {
$event->getOutput()->writeln(sprintf(
' %s %s (%s >> %s %s | %s | %s)',
(empty($xref->getId())) ? 'Generating' : 'Updating',
ClassUtils::getClass($xref),
$xref->getProfile()->getId(),
$xref->getRecipient()->getId(),
spl_object_hash($xref),
$xref->getOneRosterId(),
$xref->getId() ?: '-'
));
}
});
// go ahead and save the things we know want to keep
// this allows us to get ids that are needed in the following checks
$this->em->saveAll($managed);
// make a diff and reset contacts to those that are not matched
// these will need trashed as they are extra and do not match what is in the record from oneroster
$extras = array_udiff($xrefs, $managed, static function (ProfileContact $a, ProfileContact $b) {
return ($a === $b) ? 0 : $a->getId() <=> $b->getId();
});
// need to determine which ones are actually to be discarded and which ones were manually added.
// manually added contacts should not have an id from oneroster tied to them
// NOTE: contacts added manually then found via oneroster will be attached to oneroster data and will be subject to syncing rules
foreach ($extras as $extra) {
// if there is oneroster id, we want to discard
// this means the contact is managed via oneroster and if we make it this far, the contact did not match current data
// if there is no oneroster id, it was manually added through the portal
// these items need kept around
$extra->setDiscardedAt(
$extra->getOneRosterId()
// preserve the original discard stamp if there is one
? ($extra->getDiscardedAt() ?: DateTimeUtils::now())
: ($extra->getDiscardedAt() ?: null)
);
}
// DEBUGGING
array_walk($extras, static function (ProfileContact $xref) use ($event, $profile) {
try {
$event->getOutput()->writeln(sprintf(
' %s %s (%s >> %s %s | %s | %s)',
($xref->isDiscarded()) ? 'Discarding' : 'Updating',
ClassUtils::getClass($xref),
$xref->getProfile()->getId(),
($xref->getRecipient()) ? $xref->getRecipient()->getId() : '?',
spl_object_hash($xref),
$xref->getOneRosterId(),
$xref->getId() ?: '-'
));
} catch (\Throwable $e) {
// TODO: remove this at some point if it appears to no longer be needed...
throw new \RuntimeException(sprintf(
'XREF ID: %s | XREF OR: %s | XREF PRO ID: %s | XREF REC ID: %s | PRO ID: %s',
$xref->getId() ?: '-',
$xref->getOneRosterId() ?: '-',
$xref->getProfile() ? $xref->getProfile()->getId() : '-',
$xref->getRecipient() ? $xref->getRecipient()->getId() : '-',
$profile->getId() ?: '-'
), 0, $e);
}
});
// save changes
// discarded things will be filtered out by query filters
$this->em->saveAll($extras);
}
/**
* @param OneRosterProcessEvent $event
*/
public function initPreferences(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getJob(), [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// branch on the role type
switch (true) {
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) && $user->isRoleStaff():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) && $user->isRoleFamily():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) && $user->isRoleStudent():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) && $user->isRoleCommunity():
// noop, code will continue after the switch statement
break;
default:
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// quit early
return;
}
// load up the profile, this must match something, and it should at this point...
$profile = $this->em->getRepository(Profile::class)->findOneBy([
'onerosterId' => $user->getSourcedId(),
]);
// if we don't have a profile we have a problem
if ( ! $profile) {
throw new \RuntimeException(sprintf(
'Could not load profile "%s".',
$user->getSourcedId()
));
}
// get all the contacts
// we are pulling the assocs too
// IMPORTANT: be sure to force it to pull a fresh batch; or just grab from the em maybe...
/** @var array|ProfileContact[] $contacts */
$contacts = $this->em->getRepository(ProfileContact::class)->findBy([
'profile' => $profile,
]);
// if no contacts, skip
if ( ! $contacts) {
return;
}
// loop over each contact and enable each one that applies
foreach ($contacts as $contact) {
// if the contact has already fiddled with preferences, we need to skip this in order to preserve their settings
// NOTE: we used to do this for "discarded" ones too, but in case there is a temporary data issue, we don't want to be clearing settings for people (after seeing what confusion that causes)
if ( ! $contact->isManaged()) {
// get the recipient
$recipient = $contact->getRecipient();
// do the initial setting
switch (true) {
case $recipient instanceof EmailRecipient:
$contact
->setPrimaryPreference(Preferences::PREFERENCES__EMAIL, true)
->setSecondaryPreference(Preferences::PREFERENCES__EMAIL, true)
;
break;
case $recipient instanceof PhoneRecipient && $recipient->isSms():
$contact->setPrimaryPreference(Preferences::PREFERENCES__SMS, true);
// HACK: allow defaulting of secondary sms consent to a different value if provided by customer
// NOTE: this should require the customer to show proper documentation and gathering of the consent!!!
$contact->setSecondaryPreference(
Preferences::PREFERENCES__SMS,
($user->hasMetadataEntry('_secondary_sms_consent') && ($user->getMetadataEntry('_secondary_sms_consent') === true)),
);
break;
case $recipient instanceof PhoneRecipient && $recipient->isVoice():
$contact->setPrimaryPreference(Preferences::PREFERENCES__VOICE, true);
// TODO: should we force the voice preference here to false?
break;
}
}
// pick only one phone type when multiple are there
// prefer sms
if ($contact->hasPrimarySmsPreference() && $contact->hasPrimaryVoicePreference()) {
$contact->setPrimaryPreference(
Preferences::PREFERENCES__VOICE,
false
);
}
}
// save the changes
$this->em->saveAll(
$contacts
);
}
/**
* @param OneRosterProcessEvent $event
*/
public function calculateReachability(OneRosterProcessEvent $event): void
{
// ensure we are meant to process this
if ( ! $this->checkTypes($event->getJob(), [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// get the user
$user = $event->getEntity();
if ( ! $user instanceof OneRosterUser) {
throw new \RuntimeException(sprintf(
'User is not of proper type, got "%s".',
ClassUtils::getClass($user)
));
}
// branch on the role type
switch (true) {
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) && $user->isRoleStaff():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) && $user->isRoleFamily():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) && $user->isRoleStudent():
case $this->checkTypes($event->getJob(), [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) && $user->isRoleCommunity():
// noop, code will continue after the switch statement
break;
default:
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'User role for #%s is "%s", skipping...',
$user->getSourcedId(),
$user->getRole()
));
// quit early
return;
}
// load up the profile, this must match something, and it should at this point...
$profile = $this->em->getRepository(Profile::class)->findOneBy([
'onerosterId' => $user->getSourcedId(),
]);
// if we don't have a profile we have a problem
if ( ! $profile) {
throw new \RuntimeException(sprintf(
'Could not load profile "%s".',
$user->getSourcedId()
));
}
// get all the contacts
// IMPORTANT: be sure to force it to pull a fresh batch; or just grab from the em maybe...
/** @var array|AbstractRecipient[] $contacts */
$contacts = $this->em->getRepository(AbstractRecipient::class)->findByProfile(
$profile
);
// flatten to one standing
$standing = Reachability::STANDINGS__NONE;
foreach ($contacts as $contact) {
$standing |= $contact->getStanding();
}
// set the standing and save
$this->em->save(
$profile
->setStanding($standing)
);
}
}