Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Scope, Cardinality, Type, and DictionaryItem classes #6938

Merged
merged 3 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ changes in the following format: PR #1234***

##LORIS 24.0 (Release Date: ??)
### Core
- New classes to describe a data dictionary (PR #6938)
#### Features
- Data tables may now stream data as they're loading rather than waiting
until all data has loaded. (PR #6853)
Expand Down
100 changes: 100 additions & 0 deletions src/Data/Cardinality.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
namespace LORIS\Data;

/**
* Cardinality represents the number of data points which
* apply to the scope of a data type.
*
* Since the Cardinality class represents an enumeration, the
* class is final.
*
* @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3
*/
final class Cardinality implements \JsonSerializable
{
// Valid cardinality types for data to apply to.

/**
* A Unique Cardinality signifies that the data is unique
* across the scope. Examples of unique data are CandID
* for the candidate scope or VisitLabel for the Session
driusan marked this conversation as resolved.
Show resolved Hide resolved
* scope.
*/
const UNIQUE = 1;

/**
* A Single Cardinality signifies that each data point in
* the scope should have exactly one value. For instance,
* date of birth for a candidate in the candidate scope.
*/
const SINGLE = 2;

/**
* An Optional Cardinality signifies that each data point
* in the scope may have zero or one value. For instance,
* the date of death for a candidate in the candidate scope.
*/
const OPTIONAL = 3;

/**
* A Many Cardinality signifies that each data point will
* have zero or more values associated. For instance,
* the T1 scans acquired at a session.
Comment on lines +40 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like the example is a bit too specific, why not use something like For instance, site affiliations for a user for us non-imaging people

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind changing the example but I think site/project affiliations is a bad example because there's no "user" scoped data, and the example might get confusing since site/project affiliations exist on both candidate and session scopes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about using the candidate/session relationship then ? a candidate can have 0 to many sessions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be confusing too though, because there's the session scope and there's no variable named "Session", and session based data is usually described as 1:1 with a session scope, not 1:many with the candidate.

*/
const MANY = 4;

protected $cardinality;

/**
* Constructs a Scope object. $scope should be a class constant
* to construct the scope for, not an int literal.
*
* @param int $scope The scope
*/
public function __construct(int $card)
{
switch ($card) {
case self::UNIQUE: // fallthrough
case self::SINGLE: // fallthrough
case self::OPTIONAL: // fallthrough
case self::MANY: // fallthrough
$this->cardinality = $card;
break;
default:
throw new \DomainException("Invalid cardinality");
}
}

/**
* Convert the enumeration from a memory-friendly integer to a
* human-readable string when used in a string context.
*
* @return string
*/
public function __toString() : string
{
switch ($this->cardinality) {
case self::UNIQUE: // fallthrough
return "unique";
case self::SINGLE: // fallthrough
return "single";
case self::OPTIONAL: // fallthrough
return "optional";
case self::MANY: // fallthrough
return "many";
default:
return "invalid cardinality";
}
}

/**
* Implement the JsonSerializable interface by
* converting to a string
*
* @return string
*/
public function jsonSerialize() : string
{
return $this->__toString();
}
}
78 changes: 78 additions & 0 deletions src/Data/Dictionary/Category.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace LORIS\Data\Dictionary;

/**
* A \LORIS\Data\Dictionary\Category represents a grouping of
* DictionaryItems.
*
* @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3
*/
class Category
{
protected $name;
protected $description;
protected $items = null;

/**
* Construct a dictionary Category
*
* @param string $name The machine name of the category
* @param string $desc The human readable description of
* the category
* @param ?DictionaryItem[] $items An optional iterable of items which
* the category contains.
*/
public function __construct(string $name, string $desc, ?iterable $items = null)
{
$this->name = $name;
$this->description = $desc;
$this->items = $items;
}

/**
* Return the name of the Category
*
* @return string
*/
public function getName() : string
{
return $this->name;
}

/**
* Return the human readable description of the Category
*
* @return string
*/
public function getDescription() : string
{
return $this->description;
}

/**
* Return the items which belong to the Category
*
* @return ?DictionaryItem[]
maltheism marked this conversation as resolved.
Show resolved Hide resolved
*/
public function getItems() : ?iterable
{
return $this->items;
}

/**
* Returns a new Category identical to this category, but with
* the items populated with $items. This can be used when the items
* were not yet known at the time the constructor was called.
*
* @param DictionaryItem[] $items The items to add to the new Category
*
* @return Category
*/
public function withItems(iterable $items) : Category
{
$c = clone($this);
$c->items = $items;
return $c;
}
}
117 changes: 117 additions & 0 deletions src/Data/Dictionary/DictionaryItem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace LORIS\Data\Dictionary;

use \LORIS\Data\Scope;
use \LORIS\Data\Type;
use \LORIS\Data\Cardinality;

/**
* A DictionaryItem represents a description of a type of data
* managed by LORIS.
*
* @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3
*/
class DictionaryItem implements \LORIS\StudyEntities\AccessibleResource
xlecours marked this conversation as resolved.
Show resolved Hide resolved
{
protected $name;
protected $description;
protected $scope;
protected $type;

/**
* Construct a DictionaryItem with the given parameters
*
* @param string $name The field name of the dictionary item
* @param string $desc The dictionary item's description
* @param Scope $scope The scope to which this DictionaryItem
* applies
* @param Type $t The data type of this dictionary item
* @param Cardinality $c The data cardinality
*/
public function __construct(
string $name,
string $desc,
Scope $scope,
Type $t,
Cardinality $c
Comment on lines +36 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why single letter variables, i'm not a big fan and in the long run they always tend to be a hindrance (same goes for shortcuts like cand for candidate...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just the argument name in the constructor, the context only lasts long enough to assign it to a property right below.. the property name is the full word.

) {
$this->name = $name;
$this->description = $desc;
$this->scope = $scope;
$this->type = $t;
$this->cardinality = $c;
}

/**
* Return the field name of this DictionaryItem
*
* @return string
*/
public function getName() : string
{
return $this->name;
}

/**
* Return a human readable description of this DictionaryItem.
*
* @return string
*/
public function getDescription() : string
{
return $this->description;
}

/**
* Return the data scope at which the data for this DictionaryItem
* applies.
*
* @return Scope
*/
public function getScope() : Scope
{
return $this->scope;
}

/**
* Return the data type for the data which this DictionaryItem
* describes.
*
* @return \LORIS\Data\Type
*/
public function getDataType() : \LORIS\Data\Type
{
return $this->type;
}

/**
* Return the data cardinality of this DictionaryItem. ie. for
* each entity of type Scope how many pieces of data should
* exist for this DictionaryItem.
*
* @return \LORIS\Data\Cardinality
*/
public function getCardinality() : \LORIS\Data\Cardinality
{
return $this->cardinality;
}

/**
* The DictionaryItem instance implements the AccessibleResource
* interface in order to make it possible to restrict items per
* user. However, by default DictionaryItems are accessible by
* all users. In order to restrict access to certain items, a
* module would need to extend this class and override the
* isAccessibleBy method with its prefered business logic.
*
* @param \User $user The user whose access should be
* validated
*
* @return bool
*/
public function isAccessibleBy(\User $user): bool
{
return true;
}
}
67 changes: 67 additions & 0 deletions src/Data/Scope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
namespace LORIS\Data;

/**
* A Scope is an enumeration class which represents the scope
* that a piece of data may apply to in LORIS.
*
* The Scope class is final because the list of enumeration types
* can not be dynamically extended without modifying all places
* that must deal with the enumeration options.
Comment on lines +8 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems limiting to projects wanting to define their own scopes... why not a database table defining the constants...

would avoid overriding/overloading specially since we dont offer any easy overriding structure for the src directory

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scopes can't be extensible because everywhere in the code that deals with the data needs to understand how to deal with that scope. Let's say a module added a scope named "foobar" and someone accessed that a variable with that scope in the DQT. Questions like "how should this be displayed? How does it relate back to candidates? How does it relate to sessions? Does it need to show a list of sessions to select? How does it get displayed in the results table? How do I join it with candidate scoped data to show in a row?" need to be understood by the code to build the interface and display results.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(if you have a suggestion for how to make the comment more clear I'll update it.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no your right... I'm just thinking if there is a way we can add project defined scopes in the same way. my immediate interest is the Biospecimen scope for CBIGR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could come later though

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are similar problems with things like imaging QC. "Pass/Fail" is really file scoped data but there is no file scope. For now I've defined them as "many" cardinality scoped to the session.

*
* @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3
*/
final class Scope implements \JsonSerializable
{
// Valid scopes for data to apply to.
const CANDIDATE = 1;
const SESSION = 2;

/**
* The value of the current scope instance
*/
protected $scope;

/**
* Constructs a Scope object. $scope should be a class constant
* to construct the scope for, not an int literal.
*
* @param int $scope The scope
*/
public function __construct(int $scope)
{
switch ($scope) {
case self::CANDIDATE: // fallthrough
maltheism marked this conversation as resolved.
Show resolved Hide resolved
case self::SESSION:
$this->scope = $scope;
break;
default:
throw new \DomainException("Invalid scope");
}
}

/**
* Convert the enumeration from a memory-friendly integer to a
* human-readable string when used in a string context.
*
* @return string
*/
public function __toString() : string
{
switch ($this->scope) {
case self::CANDIDATE:
return "candidate";
case self::SESSION:
return "session";
default:
// This shouldn't happen since the constructor threw an
// exception for an invalid value.
return "invalid scope";
}
}

public function jsonSerialize()
{
return $this->__toString();
}
}