src/Cms/CoreBundle/Entity/AbstractOneRosterEntity.php line 49

Open in your IDE?
  1. <?php
  2. namespace Cms\CoreBundle\Entity;
  3. use Cms\CoreBundle\Entity\OneRoster\OneRosterOrg;
  4. use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
  5. use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableInterface;
  6. use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableTrait;
  7. use Cms\CoreBundle\Model\OneRosterStateBitwise;
  8. use Cms\TenantBundle\Entity\TenantedEntities\ExternalTenantedEntity;
  9. use DateTime;
  10. use Doctrine\ORM\Mapping as ORM;
  11. /**
  12.  * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452007
  13.  *
  14.  * Class AbstractOneRosterEntity
  15.  * @package Cms\CoreBundle\Entity
  16.  *
  17.  * @ORM\Entity(
  18.  *     repositoryClass = "Cms\CoreBundle\Doctrine\OneRosterEntityRepository"
  19.  * )
  20.  * @ORM\InheritanceType("SINGLE_TABLE")
  21.  * @ORM\DiscriminatorColumn(
  22.  *     name = "_discr",
  23.  *     type = "string",
  24.  * )
  25.  * @ORM\DiscriminatorMap({
  26.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterAcademicSession::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterAcademicSession",
  27.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterClass::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterClass",
  28.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterCourse::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterCourse",
  29.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterEnrollment::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterEnrollment",
  30.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterOrg::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterOrg",
  31.  *     Cms\CoreBundle\Entity\OneRoster\OneRosterUser::DISCR = "Cms\CoreBundle\Entity\OneRoster\OneRosterUser",
  32.  * })
  33.  * @ORM\Table(
  34.  *     name = "cms__one_roster__entity",
  35.  *     uniqueConstraints = {
  36.  *         @ORM\UniqueConstraint(
  37.  *             name = "uidx__tenant__sourcedId",
  38.  *             columns = {
  39.  *                 "tenant",
  40.  *                 "sourcedId",
  41.  *             },
  42.  *         ),
  43.  *     },
  44.  * )
  45.  */
  46. abstract class AbstractOneRosterEntity extends ExternalTenantedEntity implements TimestampableInterface
  47. {
  48.     use TimestampableTrait;
  49.     // NOTE: array order matters!!!
  50.     const ONEROSTER_TYPES = [
  51.         OneRosterOrg::ONEROSTER_TYPE => OneRosterOrg::class,
  52.         // OneRosterAcademicSession::ONEROSTER_TYPE => OneRosterAcademicSession::class,
  53.         OneRosterUser::ONEROSTER_TYPE => OneRosterUser::class,
  54.         // OneRosterCourse::ONEROSTER_TYPE => OneRosterCourse::class,
  55.         // OneRosterClass::ONEROSTER_TYPE => OneRosterClass::class,
  56.         // OneRosterEnrollment::ONEROSTER_TYPE => OneRosterEnrollment::class,
  57.     ];
  58.     const ONEROSTER_TYPE null;
  59.     const DISCR null;
  60.     const NON_ONE_ROSTER_FIELDS = [
  61.         'tenant',
  62.         'createdAt',
  63.         'updatedAt',
  64.         'touchedAt',
  65.         'state',
  66.         'checksum',
  67.     ];
  68.     /**
  69.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452021
  70.      */
  71.     const ENUMS__CLASS_TYPE__HOMEROOM 'homeroom';
  72.     const ENUMS__CLASS_TYPE__SCHEDULED 'scheduled';
  73.     /**
  74.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452022
  75.      */
  76.     const ENUMS__GENDER__MALE 'male';
  77.     const ENUMS__GENDER__FEMALE 'female';
  78.     /**
  79.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452023
  80.      */
  81.     const ENUMS__IMPORTANCE__PRIMARY 'primary';
  82.     const ENUMS__IMPORTANCE__SECONDARY 'secondary';
  83.     /**
  84.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452024
  85.      */
  86.     const ENUMS__ORG_TYPE__DEPARTMENT 'department';
  87.     const ENUMS__ORG_TYPE__SCHOOL 'school';
  88.     const ENUMS__ORG_TYPE__DISTRICT 'district';
  89.     const ENUMS__ORG_TYPE__LOCAL 'local';
  90.     const ENUMS__ORG_TYPE__STATE 'state';
  91.     const ENUMS__ORG_TYPE__NATIONAL 'national';
  92.     /**
  93.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452025
  94.      */
  95.     const ENUMS__ROLE_TYPE = [
  96.         self::ENUMS__ROLE_TYPE__ADMINISTRATOR,
  97.         self::ENUMS__ROLE_TYPE__AIDE,
  98.         self::ENUMS__ROLE_TYPE__GUARDIAN,
  99.         self::ENUMS__ROLE_TYPE__PARENT,
  100.         self::ENUMS__ROLE_TYPE__PROCTOR,
  101.         self::ENUMS__ROLE_TYPE__RELATIVE,
  102.         self::ENUMS__ROLE_TYPE__STUDENT,
  103.         self::ENUMS__ROLE_TYPE__TEACHER,
  104.         // custom
  105.         self::ENUMS__ROLE_TYPE__USER,
  106.         self::ENUMS__ROLE_TYPE__STAFF,
  107.         self::ENUMS__ROLE_TYPE__COMMUNITY,
  108.     ];
  109.     const ENUMS__ROLE_TYPE__USER 'user';// HACK: this was added to address InfiniteCampus missing school org relationships for family user types...
  110.     const ENUMS__ROLE_TYPE__STAFF 'staff';// CUSTOM: generic staff role
  111.     const ENUMS__ROLE_TYPE__COMMUNITY 'community';// CUSTOM: generic role for non-parent, non-staff people
  112.     const ENUMS__ROLE_TYPE__ADMINISTRATOR 'administrator';
  113.     const ENUMS__ROLE_TYPE__AIDE 'aide';
  114.     const ENUMS__ROLE_TYPE__GUARDIAN 'guardian';
  115.     const ENUMS__ROLE_TYPE__PARENT 'parent';
  116.     const ENUMS__ROLE_TYPE__PROCTOR 'proctor';
  117.     const ENUMS__ROLE_TYPE__RELATIVE 'relative';
  118.     const ENUMS__ROLE_TYPE__STUDENT 'student';
  119.     const ENUMS__ROLE_TYPE__TEACHER 'teacher';
  120.     /**
  121.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452026
  122.      */
  123.     const ENUMS__SCORE_STATUS__EXEMPT 'exempt';
  124.     const ENUMS__SCORE_STATUS__FULLY_GRADED 'fully graded';
  125.     const ENUMS__SCORE_STATUS__NOT_SUBMITTED 'not submitted';
  126.     const ENUMS__SCORE_STATUS__PARTIALLY_GRADED 'partially graded';
  127.     const ENUMS__SCORE_STATUS__SUBMITTED 'submitted';
  128.     /**
  129.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452027
  130.      */
  131.     const ENUMS__SESSION_TYPE__GRADING_PERIOD 'gradingPeriod';
  132.     const ENUMS__SESSION_TYPE__SEMESTER 'semester';
  133.     const ENUMS__SESSION_TYPE__SCHOOL_YEAR 'schoolYear';
  134.     const ENUMS__SESSION_TYPE__TERM 'term';
  135.     /**
  136.      * @see http://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452028
  137.      */
  138.     const ENUMS__STATUS_TYPE__ACTIVE 'active';
  139.     const ENUMS__STATUS_TYPE__TO_BE_DELETED 'tobedeleted';
  140.     /**
  141.      * @see https://ceds.ed.gov/CEDSElementDetails.aspx?TermId=7100
  142.      */
  143.     const CEDS__GRADES = [
  144.         self::CEDS__GRADES__INFANT_TODDLER,
  145.         self::CEDS__GRADES__PRESCHOOL,
  146.         self::CEDS__GRADES__PREKINDERGARTEN,
  147.         self::CEDS__GRADES__TRANSITIONAL_KINDERGARTEN,
  148.         self::CEDS__GRADES__KINDERGARTEN,
  149.         self::CEDS__GRADES__FIRST_GRADE,
  150.         self::CEDS__GRADES__SECOND_GRADE,
  151.         self::CEDS__GRADES__THIRD_GRADE,
  152.         self::CEDS__GRADES__FOURTH_GRADE,
  153.         self::CEDS__GRADES__FIFTH_GRADE,
  154.         self::CEDS__GRADES__SIXTH_GRADE,
  155.         self::CEDS__GRADES__SEVENTH_GRADE,
  156.         self::CEDS__GRADES__EIGTH_GRADE,
  157.         self::CEDS__GRADES__NINTH_GRADE,
  158.         self::CEDS__GRADES__TENTH_GRADE,
  159.         self::CEDS__GRADES__ELEVENTH_GRADE,
  160.         self::CEDS__GRADES__TWELFTH_GRADE,
  161.         self::CEDS__GRADES__GRADE_13,
  162.         self::CEDS__GRADES__POSTSECONDARY,
  163.         self::CEDS__GRADES__UNGRADED,
  164.         self::CEDS__GRADES__OTHER,
  165.     ];
  166.     const CEDS__GRADES__INFANT_TODDLER 'IT';
  167.     const CEDS__GRADES__PRESCHOOL 'PR';
  168.     const CEDS__GRADES__PREKINDERGARTEN 'PK';
  169.     const CEDS__GRADES__TRANSITIONAL_KINDERGARTEN 'TK';
  170.     const CEDS__GRADES__KINDERGARTEN 'KG';
  171.     const CEDS__GRADES__FIRST_GRADE '01';
  172.     const CEDS__GRADES__SECOND_GRADE '02';
  173.     const CEDS__GRADES__THIRD_GRADE '03';
  174.     const CEDS__GRADES__FOURTH_GRADE '04';
  175.     const CEDS__GRADES__FIFTH_GRADE '05';
  176.     const CEDS__GRADES__SIXTH_GRADE '06';
  177.     const CEDS__GRADES__SEVENTH_GRADE '07';
  178.     const CEDS__GRADES__EIGTH_GRADE '08';
  179.     const CEDS__GRADES__NINTH_GRADE '09';
  180.     const CEDS__GRADES__TENTH_GRADE '10';
  181.     const CEDS__GRADES__ELEVENTH_GRADE '11';
  182.     const CEDS__GRADES__TWELFTH_GRADE '12';
  183.     const CEDS__GRADES__GRADE_13 '13';
  184.     const CEDS__GRADES__POSTSECONDARY 'PS';
  185.     const CEDS__GRADES__UNGRADED 'UG';
  186.     const CEDS__GRADES__OTHER 'Other';
  187.     /**
  188.      * @var int|null
  189.      *
  190.      * @ORM\Column(
  191.      *     type = "integer",
  192.      *     options = {
  193.      *         "unsigned" = true,
  194.      *     },
  195.      * )
  196.      * @ORM\Id
  197.      * @ORM\GeneratedValue(
  198.      *     strategy = "AUTO",
  199.      * )
  200.      */
  201.     protected ?int $id null;
  202.     /**
  203.      * Tracks how the data has changed, if any changes.
  204.      *
  205.      * @var OneRosterStateBitwise|null
  206.      *
  207.      * @ORM\Column(
  208.      *     type = "bitwise_one_roster_state",
  209.      *     nullable = false,
  210.      * )
  211.      */
  212.     protected OneRosterStateBitwise $state;
  213.     /**
  214.      * Checksum that is computed from trackable details in order to more effectively detect changes.
  215.      *
  216.      * @var string|null
  217.      *
  218.      * @ORM\Column(
  219.      *     type = "string",
  220.      *     nullable = true,
  221.      * )
  222.      */
  223.     protected ?string $checksum null;
  224.     /**
  225.      * OneRoster data field.
  226.      *
  227.      * @var string|null
  228.      *
  229.      * @ORM\Column(
  230.      *     type = "string",
  231.      *     nullable = false,
  232.      * )
  233.      */
  234.     protected ?string $sourcedId null;
  235.     /**
  236.      * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452028
  237.      *
  238.      * OneRoster data field.
  239.      *
  240.      * @var string|null
  241.      *
  242.      * @ORM\Column(
  243.      *     type = "string",
  244.      *     nullable = false,
  245.      * )
  246.      */
  247.     protected ?string $status null;
  248.     /**
  249.      * OneRoster data field.
  250.      *
  251.      * @var DateTime|null
  252.      *
  253.      * @ORM\Column(
  254.      *     type = "datetime",
  255.      *     nullable = false,
  256.      * )
  257.      */
  258.     protected ?DateTime $dateLastModified null;
  259.     /**
  260.      * OneRoster data field.
  261.      *
  262.      * @var array
  263.      *
  264.      * @ORM\Column(
  265.      *     type = "json",
  266.      *     nullable = false,
  267.      * )
  268.      */
  269.     protected array $metadata = [];
  270.     /**
  271.      * @var OneRosterSync|null
  272.      *
  273.      * @ORM\ManyToOne(
  274.      *     targetEntity = "Cms\CoreBundle\Entity\OneRosterSync",
  275.      * )
  276.      * @ORM\JoinColumn(
  277.      *     name = "sync",
  278.      *     referencedColumnName = "id",
  279.      *     nullable = true,
  280.      *     onDelete = "CASCADE",
  281.      * )
  282.      */
  283.     protected ?OneRosterSync $sync null;
  284.     /**
  285.      * @var OneRosterJob|null
  286.      *
  287.      * @ORM\ManyToOne(
  288.      *     targetEntity = "Cms\CoreBundle\Entity\OneRosterJob",
  289.      *     inversedBy = "oneRosterEntities",
  290.      * )
  291.      * @ORM\JoinColumn(
  292.      *     name = "job",
  293.      *     referencedColumnName = "id",
  294.      *     nullable = true,
  295.      *     onDelete = "SET NULL",
  296.      * )
  297.      */
  298.     protected ?OneRosterJob $job null;
  299.     /**
  300.      *
  301.      */
  302.     public function __construct()
  303.     {
  304.         $this->state = new OneRosterStateBitwise();
  305.     }
  306.     /**
  307.      * @param array $data
  308.      * @return $this
  309.      */
  310.     public function merge(array $data): self
  311.     {
  312.         foreach ($data as $key => $value) {
  313.             if ( ! in_array($keyself::NON_ONE_ROSTER_FIELDS)) {
  314.                 $func sprintf(
  315.                     'set%s',
  316.                     ucfirst($key)
  317.                 );
  318.                 if (method_exists($this$func)) {
  319.                     $this->$func($value);
  320.                 }
  321.             }
  322.         }
  323.         return $this;
  324.     }
  325.     /**
  326.      * Gets an array of One Roster data for this object
  327.      *
  328.      * @return array
  329.      */
  330.     public function data(): array
  331.     {
  332.         $vars get_object_vars($this);
  333.         foreach (self::NON_ONE_ROSTER_FIELDS as $filter) {
  334.             if (array_key_exists($filter$vars)) {
  335.                 unset($vars[$filter]);
  336.             }
  337.         }
  338.         ksort($vars);
  339.         return $vars;
  340.     }
  341.     /**
  342.      * @return OneRosterSync|null
  343.      */
  344.     public function getSync(): ?OneRosterSync
  345.     {
  346.         return $this->sync;
  347.     }
  348.     /**
  349.      * @param OneRosterSync|null $sync
  350.      * @return $this
  351.      */
  352.     public function setSync(?OneRosterSync $sync): self
  353.     {
  354.         $this->sync $sync;
  355.         return $this;
  356.     }
  357.     /**
  358.      * @return OneRosterJob|null
  359.      */
  360.     public function getJob(): ?OneRosterJob
  361.     {
  362.         return $this->job;
  363.     }
  364.     /**
  365.      * @param OneRosterJob|null $job
  366.      * @return $this
  367.      */
  368.     public function setJob(?OneRosterJob $job): self
  369.     {
  370.         $this->job $job;
  371.         return $this;
  372.     }
  373.     /**
  374.      * @return OneRosterStateBitwise
  375.      */
  376.     public function getState(): OneRosterStateBitwise
  377.     {
  378.         return $this->state;
  379.     }
  380.     /**
  381.      * @param OneRosterStateBitwise $value
  382.      * @return $this
  383.      */
  384.     public function setState(OneRosterStateBitwise $value): self
  385.     {
  386.         $this->state $value;
  387.         return $this;
  388.     }
  389.     /**
  390.      * @return string|null
  391.      */
  392.     public function getChecksum(): ?string
  393.     {
  394.         return $this->checksum;
  395.     }
  396.     /**
  397.      * @param string|null $value
  398.      * @return $this
  399.      */
  400.     public function setChecksum(?string $value): self
  401.     {
  402.         $this->checksum $value;
  403.         return $this;
  404.     }
  405.     /**
  406.      * @return string|null
  407.      */
  408.     public function getSourcedId(): ?string
  409.     {
  410.         return $this->sourcedId;
  411.     }
  412.     /**
  413.      * @param string $value
  414.      * @return $this
  415.      */
  416.     public function setSourcedId(string $value): self
  417.     {
  418.         $this->sourcedId $value;
  419.         return $this;
  420.     }
  421.     /**
  422.      * @return string|null
  423.      */
  424.     public function getStatus(): ?string
  425.     {
  426.         return $this->status;
  427.     }
  428.     /**
  429.      * @param string $value
  430.      * @return $this
  431.      */
  432.     public function setStatus(string $value): self
  433.     {
  434.         $this->status $value;
  435.         return $this;
  436.     }
  437.     /**
  438.      * @return bool
  439.      */
  440.     public function isStatusActive(): bool
  441.     {
  442.         return ($this->getStatus() === self::ENUMS__STATUS_TYPE__ACTIVE);
  443.     }
  444.     /**
  445.      * @return bool
  446.      */
  447.     public function isStatusToBeDeleted(): bool
  448.     {
  449.         return ($this->getStatus() === self::ENUMS__STATUS_TYPE__TO_BE_DELETED);
  450.     }
  451.     /**
  452.      * @return DateTime|null
  453.      */
  454.     public function getDateLastModified(): ?DateTime
  455.     {
  456.         return $this->dateLastModified;
  457.     }
  458.     /**
  459.      * @param DateTime|string $value
  460.      * @return $this
  461.      */
  462.     public function setDateLastModified($value): self
  463.     {
  464.         $this->dateLastModified $this->parseDateTime($value);
  465.         return $this;
  466.     }
  467.     /**
  468.      * @return array
  469.      */
  470.     public function getMetadata(): array
  471.     {
  472.         return $this->metadata;
  473.     }
  474.     /**
  475.      * @param array $value
  476.      * @return $this
  477.      */
  478.     public function setMetadata(array $value): self
  479.     {
  480.         $this->metadata $this->parseStruct($value);
  481.         asort($this->metadata);
  482.         return $this;
  483.     }
  484.     /**
  485.      * @param string $key
  486.      * @return bool
  487.      */
  488.     public function hasMetadataEntry(string $key): bool
  489.     {
  490.         // TODO: this would not allow for things like "0" string and such, probably need to do a different check than empty...
  491.         return $this->metadata && array_key_exists($key$this->metadata) && ! empty($this->metadata[$key]);
  492.     }
  493.     /**
  494.      * @param string $key
  495.      * @return mixed
  496.      */
  497.     public function getMetadataEntry(string $key)
  498.     {
  499.         if ( ! $this->hasMetadataEntry($key)) {
  500.             throw new \Exception();
  501.         }
  502.         return $this->metadata[$key];
  503.     }
  504.     /**
  505.      * @param string $key
  506.      * @param mixed $value
  507.      * @return $this
  508.      */
  509.     public function setMetadataEntry(string $key$value): self
  510.     {
  511.         $this->metadata[$key] = $value;
  512.         return $this;
  513.     }
  514.     /**
  515.      * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452032
  516.      *
  517.      * @param string|DateTime $value
  518.      * @return DateTime
  519.      */
  520.     protected function parseDateTime($value): DateTime
  521.     {
  522.         $value preg_replace('/\\.[0-9]+Z$/''Z'$value);
  523.         if (is_string($value)) {
  524.             $value DateTime::createFromFormat('Y-m-d\\TH:i:s\\Z'$value);
  525.         }
  526.         if ( ! $value instanceof DateTime) {
  527.             throw new \Exception();
  528.         }
  529.         return $value;
  530.     }
  531.     /**
  532.      * TODO: do we need to treat this as if it were in the customer's timezone, since dates may not be in utc?
  533.      *
  534.      * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452032
  535.      *
  536.      * @param string|DateTime $value
  537.      * @return DateTime
  538.      */
  539.     protected function parseDate($value): DateTime
  540.     {
  541.         if (is_string($value)) {
  542.             $value DateTime::createFromFormat('Y-m-d\\TH:i:s\\Z'$value.'T00:00:00Z');
  543.         }
  544.         if ( ! $value instanceof DateTime) {
  545.             throw new \Exception();
  546.         }
  547.         return $value;
  548.     }
  549.     /**
  550.      * @param array $data
  551.      * @param callable|null $callback
  552.      * @return array
  553.      */
  554.     protected function parseArray(array $data, ?callable $callback null): array
  555.     {
  556.         if (is_callable($callback)) {
  557.             usort($data$callback);
  558.         } else {
  559.             sort($data);
  560.         }
  561.         return $data;
  562.     }
  563.     /**
  564.      * @param array $data
  565.      * @return array
  566.      */
  567.     protected function parseStruct(array $data): array
  568.     {
  569.         ksort($data);
  570.         return $data;
  571.     }
  572.     /**
  573.      * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452031
  574.      *
  575.      * @param array $ref
  576.      * @return array
  577.      */
  578.     protected function parseGuidRef(array $ref): array
  579.     {
  580.         return $this->parseStruct([
  581.             'sourcedId' => $ref['sourcedId'],
  582.             'type' => $ref['type'],
  583.         ]);
  584.     }
  585.     /**
  586.      * @see https://www.imsglobal.org/oneroster-v11-final-specification#_Toc480452031
  587.      *
  588.      * @param array|array[] $refs
  589.      * @return array
  590.      */
  591.     protected function parseGuidRefs(array $refs): array
  592.     {
  593.         return $this->parseArray(
  594.             array_map(
  595.                 function (array $ref) {
  596.                     return self::parseGuidRef($ref);
  597.                 },
  598.                 $refs
  599.             ),
  600.             function (array $a, array $b) {
  601.                 return strcmp($a['sourcedId'], $b['sourcedId']);
  602.             }
  603.         );
  604.     }
  605. }