Commit a5870703 authored by srees's avatar srees
Browse files

Modify for best practices: SOLID, namespacing, autoloading

parent 8ca964b1
# ZenodoPHP # ZenodoPHP
A PHP library for utilizing the Zenodo API. Requires >= PHP 7.0. A PHP library for utilizing the Zenodo API. Requires >= PHP 7.1.
Zenodo is part of CERN and provides permanent archive and Digital Object Identifier (DOI) capabilities for free. You can sign up separately for their production environment at https://zenodo.org and for their sandbox environment at https://sandbox.zenodo.org Zenodo is part of CERN and provides permanent archive and Digital Object Identifier (DOI) capabilities (through DataCite) for free. You can sign up separately for their production environment at https://zenodo.org and for their sandbox environment at https://sandbox.zenodo.org
You will need to have an account with Zenodo to utilize this library for their REST API, which is documented at https://developers.zenodo.org. This library follow the documentation posted as of April 2020, with the exception of a couple minor details that Zenodo had not yet updated their documentation with. You will need to have an account with Zenodo to utilize this library for their REST API, which is documented at https://developers.zenodo.org. This library follows the documentation posted as of April 2020, with the exception of a couple minor details that Zenodo had not yet updated their documentation with.
This library was developed by Stephen Rees for the Cyber-Physical Systems Virtual Organization (https://cps-vo.org), Institute for Software Integrated Systems (https://www.isis.vanderbilt.edu), Vanderbilt University (https://vanderbilt.edu) under National Science Foundation award number 1521617 (https://nsf.gov/awardsearch/showAward?AWD_ID=1521617).
Licensing is under review at this time. The intention is to open-source this library, and the library will be updated with licensing information when review is complete.
# Example 1 - Retrieve all existing depositions # Example 1 - Retrieve all existing depositions
```php ```php
include_once('zenodoAPI.php'); require_once(__DIR__ . '/includes/zenodoAPI.php');
$sandboxtoken = '';//your sandbox token
$connection = new zenodoConnection($sandboxtoken, 'sandbox'); use zenodoAPI\zenodoConnection;
use zenodoAPI\zenodoDepositionCollection;
$sandboxtoken = '';//your sandbox token
$connection = new zenodoConnection($sandboxtoken, 'sandbox');
$collection_obj = new ZenodoDepositionCollection($connection); $collection_obj = new ZenodoDepositionCollection($connection);
try{ try{
$collection = $collection_obj->list_depositions();//returns an array of deposition objects $collection = $collection_obj->list_depositions();//returns an array of deposition objects
}catch (RuntimeException $e){ }
catch (RuntimeException $e) {
//you should only have to catch the runtime exception the first time you request data from Zenodo //you should only have to catch the runtime exception the first time you request data from Zenodo
echo $e->getMessage(); echo $e->getMessage();
echo var_export($connection->last_response());//this is where you will find details on any errors echo var_export($connection->last_response());//this is where you will find details on any errors
return; return;
}catch(LogicException $e){ }
catch (LogicException $e) {
//you should catch a logic exception every time you request data from Zenodo. //you should catch a logic exception every time you request data from Zenodo.
echo $e->getMessage(); echo $e->getMessage();
echo var_export($connection->last_response()); echo var_export($connection->last_response());
...@@ -32,7 +39,12 @@ echo var_export($collection); ...@@ -32,7 +39,12 @@ echo var_export($collection);
# Example 2 - Retrieve a specific deposition # Example 2 - Retrieve a specific deposition
```php ```php
//leaving off the exception handling this time //leaving off the exception handling this time
require_once(__DIR__ . '/includes/zenodoAPI.php');
use zenodoAPI\zenodoConnection;
use zenodoAPI\zenodoDeposition;
$sandboxtoken = '';//your sandbox token
$connection = new zenodoConnection('your_sandbox_token', 'sandbox'); $connection = new zenodoConnection('your_sandbox_token', 'sandbox');
$deposition = new zenodoDeposition($connection); $deposition = new zenodoDeposition($connection);
$deposition->retrieve_deposition(12345);//whatever your deposition ID is... $deposition->retrieve_deposition(12345);//whatever your deposition ID is...
...@@ -40,6 +52,12 @@ echo var_export($deposition->clean());//clean returns a cloned object stripped o ...@@ -40,6 +52,12 @@ echo var_export($deposition->clean());//clean returns a cloned object stripped o
``` ```
# Example 3 - Create a deposition # Example 3 - Create a deposition
```php ```php
require_once(__DIR__ . '/includes/zenodoAPI.php');
use zenodoAPI\zenodoConnection;
use zenodoAPI\zenodoDeposition;
$sandboxtoken = '';//your sandbox token
$connection = new zenodoConnection('your_sandbox_token', 'sandbox'); $connection = new zenodoConnection('your_sandbox_token', 'sandbox');
$deposition = new zenodoDeposition($connection); $deposition = new zenodoDeposition($connection);
$deposition->create_deposition();//deposition object is now created with Zenodo and ready to be populated with metadata $deposition->create_deposition();//deposition object is now created with Zenodo and ready to be populated with metadata
...@@ -56,10 +74,10 @@ $deposition->metadata->publication_date(date('Y-m-d')); ...@@ -56,10 +74,10 @@ $deposition->metadata->publication_date(date('Y-m-d'));
$deposition->metadata->title('My First Deposition'); $deposition->metadata->title('My First Deposition');
$creator = new stdClass(); $creator = new stdClass();
$creator->name = 'Family name, Given names'; $creator->name = 'Family name, Given names';
$deposition->metadata->creators(array($creator)); $deposition->metadata->creators([$creator]);
$deposition->metadata->description('This is an example of how to create a deposition with the Zenodo API'); $deposition->metadata->description('This is an example of how to create a deposition with the Zenodo API');
$deposition->metadata->access_right('closed'); $deposition->metadata->access_right('closed');
$deposition->metadata->keywords(array('first','example')); $deposition->metadata->keywords(['first', 'example']);
$deposition->update_deposition(); $deposition->update_deposition();
//Want to publish it? This locks it against changes and deletion permanently //Want to publish it? This locks it against changes and deletion permanently
......
<?php <?php
/** @noinspection PhpUnused */ /** @noinspection PhpUnused */
/** @noinspection SpellCheckingInspection */ /** @noinspection SpellCheckingInspection */
/** @noinspection PhpUnusedPrivateMethodInspection */ /** @noinspection PhpUnusedPrivateMethodInspection */
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
namespace zenodoAPI;
class zenodoConnection{
spl_autoload_extensions(".php"); // comma-separated list
const sandBoxURL = 'https://sandbox.zenodo.org/api/'; spl_autoload_register('zenodoAPI\zenodoAPI_autoload');
const apiURL = 'https://zenodo.org/api/'; function zenodoAPI_autoload($class)
{
private $connect_URL; $filename = __DIR__ . DIRECTORY_SEPARATOR . str_replace(
private $access_token; '\\',
private $last_response; DIRECTORY_SEPARATOR,
$class
) . '.php';
public function __construct(string $_token, string $_target = ''){ if (is_file($filename)) {
if(empty($_token)){ require($filename);
throw new InvalidArgumentException('Token for zenodoConnection cannot be empty');
}
if ($_target == 'production'){//this should be exposed as a config setting in Drupal that can be additionally controlled via settings.php
$this->connect_URL = self::apiURL;
}
else{
$this->connect_URL = self::sandBoxURL;
}
$this->access_token = $_token;
$this->last_response = array();
}
public function last_response(){
return $this->last_response;
}
public function make_request($_verb = 'GET', $_path = '', $_content_type = '', $_data = ''){
$ch = curl_init($this->connect_URL . $_path);
$options = array(
CURLOPT_CUSTOMREQUEST => $_verb,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $this->access_token,
),
CURLOPT_SSL_VERIFYPEER => TRUE,
CURLOPT_SSL_VERIFYHOST => TRUE,
CURLOPT_FOLLOWLOCATION => TRUE,
CURLOPT_POSTREDIR => 3,
);
if (!empty($_content_type)){
$options[CURLOPT_HTTPHEADER][] = "Content-Type: $_content_type";
}
if (!empty($_data)){
if ($_content_type == 'application/json'){
$_data = json_encode($_data);
}
if($_content_type == 'multipart/form-data'){
$file = curl_file_create(realpath($_data), NULL, basename($_data));//TODO: do we want to include mimetype as part of this?
$_data = array('file' => $file);
}
$options[CURLOPT_POSTFIELDS] = $_data;
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$curl_info = curl_getinfo($ch);
$curl_error = curl_error($ch);
curl_close($ch);
$this->last_response = array('data' => $_data, 'response' => $response, 'curl_info' => $curl_info, 'curl_error' => $curl_error);
if ($response === FALSE){//address curl failure
throw new RuntimeException('Curl Failed. Check last_response() for details.');
}
$response_data = json_decode($response);
//responses, including errors, are returned as jSON objects -- with the exception of collections, which are returned as an array of objects
if(is_object($response_data) && isset($response_data->status) && (int) $response_data->status > 204){
throw new LogicException('Request failed. Check last_response() for details.');
}
return $response_data;
}
}
class zenodoDepositionCollection{
private $connection;
private $deposition_array;
public function __construct(zenodoConnection $_connection){
$this->connection = $_connection;
$this->deposition_array = array();
}
public function allowed_sorts():array{
return array('bestmatch' => 'Best Match Ascending', '-bestmatch' => 'Best Match Descending', 'mostrecent' => 'Most Recent Ascending', '-mostrecent' => 'Most Recent Descending');
}
public function list_depositions(bool $_flushcache = FALSE, array $params = array()):array {
//If params are sent, cache will be flushed!
/*optional query parameters:
q - string optional Search query (using Elasticsearch query string syntax).
status - string optional Filter result based on deposit status (either draft or published)
sort - string optional Sort order (bestmatch or mostrecent). Prefix with minus to change form ascending to descending (e.g. -mostrecent).
page - integer optional Page number for pagination.
size - integer optional Number of results to return per page.
*/
if(empty($this->deposition_array) || $_flushcache == TRUE || !empty($params)){
$this->deposition_array = array();
$query = '';
if(!empty($params)){
//attempt to clean
$valid_params = array();
foreach ($params as $key=>$value){
$key = strtolower($key);
$value = strtolower($value);
switch($key){
case 'q':
if(is_string($value)) $valid_params['q'] = $value;
break;
case 'status':
if(is_string($value) && ($value == 'draft' || $value == 'published')) $valid_params['status'] = $value;
break;
case 'sort':
if(is_string($value) && array_key_exists($value, $this->allowed_sorts())) $valid_params['sort'] = $value;
break;
case 'page':
if(is_int($value)) $valid_params['page'] = $value;
break;
case 'size':
if(is_int($value)) $valid_params['size'] = $value;
break;
default:
throw new InvalidArgumentException('Unknown parameter "' . $key . '" given to list_depositions');
}
}
if(!empty($valid_params)){
$query = '?' . http_build_query($valid_params);
}
}
//errors in the request will generate an exception that needs to be handled by code implementing this library
$array = $this->connection->make_request('GET', 'deposit/depositions' . $query);
if(is_array($array)){
foreach ($array as $deposition){
$zd = new zenodoDeposition($this->connection);
$zd->load($deposition);
$this->deposition_array[] = $zd;
}
}
}
return $this->deposition_array;
}
}
class zenodoDeposition{
private $connection;
private $created;
private $doi;
private $doi_url;
private $files;
private $id;
private $metadata;
private $modified;
private $owner;
private $record_id;
private $record_url;
private $state;
private $submitted;
private $title;
private $conceptrecid;
private $links;
public function __construct(zenodoConnection $_connection){
$this->connection = $_connection;
$this->created = 0;//timestamp
$this->doi = '';
$this->doi_url = '';
$this->files = array();
$this->id = 0;
$this->metadata = new zenodoDepositionMetadata();
$this->modified = 0;//timestamp
$this->owner = 0;
$this->record_id = 0;
$this->record_url = '';
$this->state = '';
$this->submitted = FALSE;
$this->title = '';
$this->conceptrecid = 0;
$this->links = new stdClass();
}
/*
* Provides a way to load the entire object from a json decoded result
* while still retaining custom getters and setters
*/
public function load(stdClass $_jsonObject){
//the ID field needs to be loaded first for child objects to load properly
$this->id($_jsonObject->id);
foreach($_jsonObject as $key=>$value){
if(method_exists($this, $key)){
$this->$key($value);
}
}
}
public function __get(string $_name){
if(isset($this->$_name)){
return $this->$_name;
}else{
throw new InvalidArgumentException('Attempted to get non-existent value from object');
}
}
/***************************
* Manual setters to address custom validation/access
*************************/
/**
* @param string $_setValue
*/
private function created(string $_setValue){
$this->created = $_setValue;
}
private function doi(string $_setValue){
$this->doi = $_setValue;
}
private function doi_url(string $_setValue){
if(filter_var($_setValue, FILTER_VALIDATE_URL)){
$this->doi_url = $_setValue;
}else{
throw new InvalidArgumentException('DOI URL failed to validate as a URL');
}
}
public function files(array $_setValue){
$new_files = array();
foreach($_setValue as $file){
if(is_object($file) && $file instanceof zenodoDepositionFile){
$new_files[] = $file;
}elseif(is_object($file) && $file instanceof stdClass){
$create = new zenodoDepositionFile($this->connection, $this->id);
$create->load($file);
$new_files[] = $create;
}else{
throw new InvalidArgumentException('Attempted to set files with invalid file object in array');
}
}
$this->files = $new_files;
}
public function id(int $_setValue){
if($this->id == 0 || $this->id == $_setValue){
$this->id = $_setValue;
}else{
throw new LogicException('Cannot change the ID of an already loaded deposition');
}
}
public function metadata($_setValue){
if(is_object($_setValue) && $_setValue instanceof zenodoDepositionMetadata){
$this->metadata = $_setValue;
}elseif(is_object($_setValue) && $_setValue instanceof stdClass){
$this->metadata = new zenodoDepositionMetadata();
$this->metadata->load($_setValue);
}else{
throw new InvalidArgumentException('Attempted to set metadata with invalid object');
}
}
private function modified(string $_setValue){
$this->modified = $_setValue;
}
private function owner(int $_setValue){
$this->owner = $_setValue;
}
private function record_id(int $_setValue){
$this->record_id = $_setValue;
}
private function record_url(string $_setValue){
if(filter_var($_setValue, FILTER_VALIDATE_URL)){
$this->record_url = $_setValue;
}else{
throw new InvalidArgumentException('Record URL failed to validate as a URL');
}
}
private function state(string $_setValue){
$allowed_states = array('unsubmitted','inprogress','done','error');
if(in_array($_setValue,$allowed_states)){
$this->state = $_setValue;
}else{
throw new InvalidArgumentException('Invalid state option provided');
}
}
private function submitted(bool $_setValue){
$this->submitted = $_setValue;
}
private function title(string $_setValue){
$this->title = $_setValue;
}
private function conceptrecid(int $_setValue){
$this->conceptrecid = $_setValue;
}
private function links(stdClass $_setValue){
foreach($_setValue as $name => $value){
$this->links->$name = $value;
}
}
public function clean():stdClass{
$clean_obj = new stdClass();
foreach(get_object_vars($this) as $name=>$value){
if($name == 'connection'){
continue;
}elseif($name == 'files'){
$clean_obj->files = array();
foreach($value as $file){
$clean_obj->files[] = $file->clean();
}
}elseif($name == 'metadata'){
$clean_obj->metadata = $value->clean();
}else{
$clean_obj->$name = $value;
}
}
return $clean_obj;
}
/*****************
* API functions
****************/
//Can accept either an empty object or a Deposition Metadata Resource
/**
* @param null $_deposition
* @throws \HttpException
* @throws \HttpResponseException
*/
public function create_deposition($_deposition = NULL){
if(is_null($_deposition) || $_deposition instanceof zenodoDepositionMetadata){
if(is_null($_deposition)){
$deposition = new stdClass();
}else{
$deposition = $_deposition->clean();
}
//errors in the request will generate an exception that needs to be handled by code implementing this library
$this->load($this->connection->make_request('POST', 'deposit/depositions', 'application/json', $deposition));
}else{
throw new InvalidArgumentException('Create deposition requires an empty object, NULL, or a zenodoDepositionMetaData object');
}
}
public function retrieve_deposition(int $_id){
//errors in the request will generate an exception that needs to be handled by code implementing this library
$this->load($this->connection->make_request('GET', 'deposit/depositions/' . $_id));
}
/*
* This will wipe out this object and replace it with the response!
* It is passed only the metadata object.
*/
public function update_deposition(){
//errors in the request will generate an exception that needs to be handled by code implementing this library
$this->load($this->connection->make_request('PUT', "deposit/depositions/$this->id", 'application/json', $this->metadata->clean()));
}
public function delete_deposition(){
//can only delete a deposition that is not yet published
$this->connection->make_request('DELETE', "deposit/depositions/$this->id");
}
public function list_files(){
$this->files($this->connection->make_request('GET', "deposit/depositions/$this->id/files"));
}
public function add_file(string $_file):zenodoDepositionFile{
if(file_exists(realpath($_file))){
$newfile = new zenodoDepositionFile($this->connection, $this->id);
$newfile->create_file($_file);
$this->files[] = $newfile;
return $newfile;
}else{
throw new InvalidArgumentException('File cannot be found');
}
}
public function sort_files(){
//it may be better to just accept an array of integers and validate them against the files we have
//first file is shown in file preview. We should pass an array of deposition file resources, each with only the id attribute.
$file_array = array();
foreach($this->files as $file){
$file_object = new stdClass();
$file_object->id = $file->id();
$file_array[] = $file_object;
}
$this->files($this->connection->make_request('PUT', "deposit/depositions/$this->id/files", 'application/json', $file_array));
}
public function publish_deposition(){
$this->load($this->connection->make_request('POST', "deposit/depositions/$this->id/actions/publish"));
}
public function edit_deposition(){
$this->load($this->connection->make_request('POST', "deposit/depositions/$this->id/actions/edit"));
}
//Alias for edit_deposition
public function unlock_deposition(){
$this->edit_deposition();
}
public function discard_deposition(){
$this->load($this->connection->make_request('POST', "deposit/depositions/$this->id/actions/discard"));
}
//Alias for discard_deposition
public function reset_deposition(){
$this->discard_deposition();
}
public function clone_deposition(){
//This returns the original deposition! The new, unpublished version can be access through the 'latest_draft' under 'links' in the response.
$this->load($this->connection->make_request('POST', "deposit/depositions/$this->id/actions/newversion"));
}
//Alias for clone
public function new_version_deposition(){
$this->clone_deposition();
}
}
class zenodoDepositionFile{
private $connection;
private $parent_id;
private $id;
private $filename;
private $filesize;
private $checksum;
public function __construct(zenodoConnection $_connection, int $_parent_id){
$this->connection = $_connection;
$this->parent_id = $_parent_id;
$this->id = '';
$this->filename = '';
$this->filesize = 0;
$this->checksum = '';
}
public function __get(string $_name){
if(isset($this->$_name)){
return $this->$_name;
}else{
throw new InvalidArgumentException('Attempted to get non-existent value from object');
}
}
public function load(stdClass $_jsonObject){
foreach($_jsonObject as $key=>$value){
if(method_exists($this, $key)){
$this->$key($value);
}
}
}
public function id(string $_setValue){
if($this->id == '' || $this->id == $_setValue){
$this->id = $_setValue;
}else{
throw new LogicException('Cannot change the ID of an already loaded file');
}
}
public function filename(string $_setValue){
$this->filename = $_setValue;
}
private function filesize(int $_setValue){
$this->filesize = $_setValue;
}
private function checksum(string $_setValue){
$this->checksum = $_setValue;
}
public function clean():stdClass{
$clean_obj = new stdClass();
foreach(get_object_vars($this) as $name=>$value){
if($name == 'connection' || $name == 'parent_id'){
continue;
}else{
$clean_obj->$name = $value;
}
}
return $clean_obj;
}
public function create_file(string $_file){
$this->load($this->connection->make_request('POST', "deposit/depositions/$this->parent_id/files", 'multipart/form-data', $_file));
}
public function retrieve_file(){