<?php
namespace Cms\CoreBundle\Entity;
use Cms\CoreBundle\Entity\OneRoster\OneRosterOrg;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableInterface;
use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableTrait;
use Cms\CoreBundle\Model\OneRosterStateBitwise;
use Cms\TenantBundle\Entity\TenantedEntities\ExternalTenantedEntity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452007
*
* Class AbstractOneRosterEntity
* @package Cms\CoreBundle\Entity
*
* @ORM\Entity(
* repositoryClass = "Cms\CoreBundle\Doctrine\OneRosterEntityRepository"
* )
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(
* name = "_discr",
* type = "string",
* )
* @ORM\DiscriminatorMap({
* Cms\CoreBundle\Entity\OneRoster\OneRosterAcademicSession::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterAcademicSession",
* Cms\CoreBundle\Entity\OneRoster\OneRosterClass::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterClass",
* Cms\CoreBundle\Entity\OneRoster\OneRosterCourse::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterCourse",
* Cms\CoreBundle\Entity\OneRoster\OneRosterEnrollment::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterEnrollment",
* Cms\CoreBundle\Entity\OneRoster\OneRosterOrg::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterOrg",
* Cms\CoreBundle\Entity\OneRoster\OneRosterUser::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterUser",
* })
* @ORM\Table(
* name = "cms__one_roster__entity",
* uniqueConstraints = {
* @ORM\UniqueConstraint(
* name = "uidx__tenant__sourcedId",
* columns = {
* "tenant",
* "sourcedId",
* },
* ),
* },
* )
*/
abstract class AbstractOneRosterEntity extends ExternalTenantedEntity implements TimestampableInterface
{
use TimestampableTrait;
// NOTE: array order matters!!!
const ONEROSTER_TYPES = [
OneRosterOrg::ONEROSTER_TYPE => OneRosterOrg::class,
// OneRosterAcademicSession::ONEROSTER_TYPE => OneRosterAcademicSession::class,
OneRosterUser::ONEROSTER_TYPE => OneRosterUser::class,
// OneRosterCourse::ONEROSTER_TYPE => OneRosterCourse::class,
// OneRosterClass::ONEROSTER_TYPE => OneRosterClass::class,
// OneRosterEnrollment::ONEROSTER_TYPE => OneRosterEnrollment::class,
];
const ONEROSTER_TYPE = null;
const DISCR = null;
const NON_ONE_ROSTER_FIELDS = [
'tenant',
'createdAt',
'updatedAt',
'touchedAt',
'state',
'checksum',
];
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452021
*/
const ENUMS__CLASS_TYPE__HOMEROOM = 'homeroom';
const ENUMS__CLASS_TYPE__SCHEDULED = 'scheduled';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452022
*/
const ENUMS__GENDER__MALE = 'male';
const ENUMS__GENDER__FEMALE = 'female';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452023
*/
const ENUMS__IMPORTANCE__PRIMARY = 'primary';
const ENUMS__IMPORTANCE__SECONDARY = 'secondary';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452024
*/
const ENUMS__ORG_TYPE__DEPARTMENT = 'department';
const ENUMS__ORG_TYPE__SCHOOL = 'school';
const ENUMS__ORG_TYPE__DISTRICT = 'district';
const ENUMS__ORG_TYPE__LOCAL = 'local';
const ENUMS__ORG_TYPE__STATE = 'state';
const ENUMS__ORG_TYPE__NATIONAL = 'national';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452025
*/
const ENUMS__ROLE_TYPE = [
self::ENUMS__ROLE_TYPE__ADMINISTRATOR,
self::ENUMS__ROLE_TYPE__AIDE,
self::ENUMS__ROLE_TYPE__GUARDIAN,
self::ENUMS__ROLE_TYPE__PARENT,
self::ENUMS__ROLE_TYPE__PROCTOR,
self::ENUMS__ROLE_TYPE__RELATIVE,
self::ENUMS__ROLE_TYPE__STUDENT,
self::ENUMS__ROLE_TYPE__TEACHER,
// custom
self::ENUMS__ROLE_TYPE__USER,
self::ENUMS__ROLE_TYPE__STAFF,
self::ENUMS__ROLE_TYPE__COMMUNITY,
];
const ENUMS__ROLE_TYPE__USER = 'user';// HACK: this was added to address InfiniteCampus missing school org relationships for family user types...
const ENUMS__ROLE_TYPE__STAFF = 'staff';// CUSTOM: generic staff role
const ENUMS__ROLE_TYPE__COMMUNITY = 'community';// CUSTOM: generic role for non-parent, non-staff people
const ENUMS__ROLE_TYPE__ADMINISTRATOR = 'administrator';
const ENUMS__ROLE_TYPE__AIDE = 'aide';
const ENUMS__ROLE_TYPE__GUARDIAN = 'guardian';
const ENUMS__ROLE_TYPE__PARENT = 'parent';
const ENUMS__ROLE_TYPE__PROCTOR = 'proctor';
const ENUMS__ROLE_TYPE__RELATIVE = 'relative';
const ENUMS__ROLE_TYPE__STUDENT = 'student';
const ENUMS__ROLE_TYPE__TEACHER = 'teacher';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452026
*/
const ENUMS__SCORE_STATUS__EXEMPT = 'exempt';
const ENUMS__SCORE_STATUS__FULLY_GRADED = 'fully graded';
const ENUMS__SCORE_STATUS__NOT_SUBMITTED = 'not submitted';
const ENUMS__SCORE_STATUS__PARTIALLY_GRADED = 'partially graded';
const ENUMS__SCORE_STATUS__SUBMITTED = 'submitted';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452027
*/
const ENUMS__SESSION_TYPE__GRADING_PERIOD = 'gradingPeriod';
const ENUMS__SESSION_TYPE__SEMESTER = 'semester';
const ENUMS__SESSION_TYPE__SCHOOL_YEAR = 'schoolYear';
const ENUMS__SESSION_TYPE__TERM = 'term';
/**
* @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452028
*/
const ENUMS__STATUS_TYPE__ACTIVE = 'active';
const ENUMS__STATUS_TYPE__TO_BE_DELETED = 'tobedeleted';
/**
* @see https://ceds.ed.gov/CEDSElementDetails.aspx?TermId=7100
*/
const CEDS__GRADES = [
self::CEDS__GRADES__INFANT_TODDLER,
self::CEDS__GRADES__PRESCHOOL,
self::CEDS__GRADES__PREKINDERGARTEN,
self::CEDS__GRADES__TRANSITIONAL_KINDERGARTEN,
self::CEDS__GRADES__KINDERGARTEN,
self::CEDS__GRADES__FIRST_GRADE,
self::CEDS__GRADES__SECOND_GRADE,
self::CEDS__GRADES__THIRD_GRADE,
self::CEDS__GRADES__FOURTH_GRADE,
self::CEDS__GRADES__FIFTH_GRADE,
self::CEDS__GRADES__SIXTH_GRADE,
self::CEDS__GRADES__SEVENTH_GRADE,
self::CEDS__GRADES__EIGTH_GRADE,
self::CEDS__GRADES__NINTH_GRADE,
self::CEDS__GRADES__TENTH_GRADE,
self::CEDS__GRADES__ELEVENTH_GRADE,
self::CEDS__GRADES__TWELFTH_GRADE,
self::CEDS__GRADES__GRADE_13,
self::CEDS__GRADES__POSTSECONDARY,
self::CEDS__GRADES__UNGRADED,
self::CEDS__GRADES__OTHER,
];
const CEDS__GRADES__INFANT_TODDLER = 'IT';
const CEDS__GRADES__PRESCHOOL = 'PR';
const CEDS__GRADES__PREKINDERGARTEN = 'PK';
const CEDS__GRADES__TRANSITIONAL_KINDERGARTEN = 'TK';
const CEDS__GRADES__KINDERGARTEN = 'KG';
const CEDS__GRADES__FIRST_GRADE = '01';
const CEDS__GRADES__SECOND_GRADE = '02';
const CEDS__GRADES__THIRD_GRADE = '03';
const CEDS__GRADES__FOURTH_GRADE = '04';
const CEDS__GRADES__FIFTH_GRADE = '05';
const CEDS__GRADES__SIXTH_GRADE = '06';
const CEDS__GRADES__SEVENTH_GRADE = '07';
const CEDS__GRADES__EIGTH_GRADE = '08';
const CEDS__GRADES__NINTH_GRADE = '09';
const CEDS__GRADES__TENTH_GRADE = '10';
const CEDS__GRADES__ELEVENTH_GRADE = '11';
const CEDS__GRADES__TWELFTH_GRADE = '12';
const CEDS__GRADES__GRADE_13 = '13';
const CEDS__GRADES__POSTSECONDARY = 'PS';
const CEDS__GRADES__UNGRADED = 'UG';
const CEDS__GRADES__OTHER = 'Other';
/**
* @var int|null
*
* @ORM\Column(
* type = "integer",
* options = {
* "unsigned" = true,
* },
* )
* @ORM\Id
* @ORM\GeneratedValue(
* strategy = "AUTO",
* )
*/
protected ?int $id = null;
/**
* Tracks how the data has changed, if any changes.
*
* @var OneRosterStateBitwise|null
*
* @ORM\Column(
* type = "bitwise_one_roster_state",
* nullable = false,
* )
*/
protected OneRosterStateBitwise $state;
/**
* Checksum that is computed from trackable details in order to more effectively detect changes.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $checksum = null;
/**
* OneRoster data field.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = false,
* )
*/
protected ?string $sourcedId = null;
/**
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452028
*
* OneRoster data field.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = false,
* )
*/
protected ?string $status = null;
/**
* OneRoster data field.
*
* @var DateTime|null
*
* @ORM\Column(
* type = "datetime",
* nullable = false,
* )
*/
protected ?DateTime $dateLastModified = null;
/**
* OneRoster data field.
*
* @var array
*
* @ORM\Column(
* type = "json",
* nullable = false,
* )
*/
protected array $metadata = [];
/**
* @var OneRosterSync|null
*
* @ORM\ManyToOne(
* targetEntity = "Cms\CoreBundle\Entity\OneRosterSync",
* )
* @ORM\JoinColumn(
* name = "sync",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "CASCADE",
* )
*/
protected ?OneRosterSync $sync = null;
/**
* @var OneRosterJob|null
*
* @ORM\ManyToOne(
* targetEntity = "Cms\CoreBundle\Entity\OneRosterJob",
* inversedBy = "oneRosterEntities",
* )
* @ORM\JoinColumn(
* name = "job",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "SET NULL",
* )
*/
protected ?OneRosterJob $job = null;
/**
*
*/
public function __construct()
{
$this->state = new OneRosterStateBitwise();
}
/**
* @param array $data
* @return $this
*/
public function merge(array $data): self
{
foreach ($data as $key => $value) {
if ( ! in_array($key, self::NON_ONE_ROSTER_FIELDS)) {
$func = sprintf(
'set%s',
ucfirst($key)
);
if (method_exists($this, $func)) {
$this->$func($value);
}
}
}
return $this;
}
/**
* Gets an array of One Roster data for this object
*
* @return array
*/
public function data(): array
{
$vars = get_object_vars($this);
foreach (self::NON_ONE_ROSTER_FIELDS as $filter) {
if (array_key_exists($filter, $vars)) {
unset($vars[$filter]);
}
}
ksort($vars);
return $vars;
}
/**
* @return OneRosterSync|null
*/
public function getSync(): ?OneRosterSync
{
return $this->sync;
}
/**
* @param OneRosterSync|null $sync
* @return $this
*/
public function setSync(?OneRosterSync $sync): self
{
$this->sync = $sync;
return $this;
}
/**
* @return OneRosterJob|null
*/
public function getJob(): ?OneRosterJob
{
return $this->job;
}
/**
* @param OneRosterJob|null $job
* @return $this
*/
public function setJob(?OneRosterJob $job): self
{
$this->job = $job;
return $this;
}
/**
* @return OneRosterStateBitwise
*/
public function getState(): OneRosterStateBitwise
{
return $this->state;
}
/**
* @param OneRosterStateBitwise $value
* @return $this
*/
public function setState(OneRosterStateBitwise $value): self
{
$this->state = $value;
return $this;
}
/**
* @return string|null
*/
public function getChecksum(): ?string
{
return $this->checksum;
}
/**
* @param string|null $value
* @return $this
*/
public function setChecksum(?string $value): self
{
$this->checksum = $value;
return $this;
}
/**
* @return string|null
*/
public function getSourcedId(): ?string
{
return $this->sourcedId;
}
/**
* @param string $value
* @return $this
*/
public function setSourcedId(string $value): self
{
$this->sourcedId = $value;
return $this;
}
/**
* @return string|null
*/
public function getStatus(): ?string
{
return $this->status;
}
/**
* @param string $value
* @return $this
*/
public function setStatus(string $value): self
{
$this->status = $value;
return $this;
}
/**
* @return bool
*/
public function isStatusActive(): bool
{
return ($this->getStatus() === self::ENUMS__STATUS_TYPE__ACTIVE);
}
/**
* @return bool
*/
public function isStatusToBeDeleted(): bool
{
return ($this->getStatus() === self::ENUMS__STATUS_TYPE__TO_BE_DELETED);
}
/**
* @return DateTime|null
*/
public function getDateLastModified(): ?DateTime
{
return $this->dateLastModified;
}
/**
* @param DateTime|string $value
* @return $this
*/
public function setDateLastModified($value): self
{
$this->dateLastModified = $this->parseDateTime($value);
return $this;
}
/**
* @return array
*/
public function getMetadata(): array
{
return $this->metadata;
}
/**
* @param array $value
* @return $this
*/
public function setMetadata(array $value): self
{
$this->metadata = $this->parseStruct($value);
asort($this->metadata);
return $this;
}
/**
* @param string $key
* @return bool
*/
public function hasMetadataEntry(string $key): bool
{
// TODO: this would not allow for things like "0" string and such, probably need to do a different check than empty...
return $this->metadata && array_key_exists($key, $this->metadata) && ! empty($this->metadata[$key]);
}
/**
* @param string $key
* @return mixed
*/
public function getMetadataEntry(string $key)
{
if ( ! $this->hasMetadataEntry($key)) {
throw new \Exception();
}
return $this->metadata[$key];
}
/**
* @param string $key
* @param mixed $value
* @return $this
*/
public function setMetadataEntry(string $key, $value): self
{
$this->metadata[$key] = $value;
return $this;
}
/**
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452032
*
* @param string|DateTime $value
* @return DateTime
*/
protected function parseDateTime($value): DateTime
{
$value = preg_replace('/\\.[0-9]+Z$/', 'Z', $value);
if (is_string($value)) {
$value = DateTime::createFromFormat('Y-m-d\\TH:i:s\\Z', $value);
}
if ( ! $value instanceof DateTime) {
throw new \Exception();
}
return $value;
}
/**
* TODO: do we need to treat this as if it were in the customer's timezone, since dates may not be in utc?
*
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452032
*
* @param string|DateTime $value
* @return DateTime
*/
protected function parseDate($value): DateTime
{
if (is_string($value)) {
$value = DateTime::createFromFormat('Y-m-d\\TH:i:s\\Z', $value.'T00:00:00Z');
}
if ( ! $value instanceof DateTime) {
throw new \Exception();
}
return $value;
}
/**
* @param array $data
* @param callable|null $callback
* @return array
*/
protected function parseArray(array $data, ?callable $callback = null): array
{
if (is_callable($callback)) {
usort($data, $callback);
} else {
sort($data);
}
return $data;
}
/**
* @param array $data
* @return array
*/
protected function parseStruct(array $data): array
{
ksort($data);
return $data;
}
/**
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452031
*
* @param array $ref
* @return array
*/
protected function parseGuidRef(array $ref): array
{
return $this->parseStruct([
'sourcedId' => $ref['sourcedId'],
'type' => $ref['type'],
]);
}
/**
* @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452031
*
* @param array|array[] $refs
* @return array
*/
protected function parseGuidRefs(array $refs): array
{
return $this->parseArray(
array_map(
function (array $ref) {
return self::parseGuidRef($ref);
},
$refs
),
function (array $a, array $b) {
return strcmp($a['sourcedId'], $b['sourcedId']);
}
);
}
}