uncgits / canvas-api-php-library
PHP Library used to interact with the Canvas API for UNCG
Requires
- php: ^7.0 || ^8.0
- nesbot/carbon: >=1.22.0
Suggests
- guzzlehttp/guzzle: 6.* recommended to make HTTP request with Guzzle adapter
README
This package is a PHP library used to interact with the Canvas REST API.
Scope
This package is not (yet) as a comprehensive interface with the Canvas API. If you would like to contribute additional functionality to this library, pull requests are very welcome.
Installation and Quick Start
To install the package: composer require uncgits/canvas-api-php-library
If you are using the built-in Guzzle driver, you will also need to make sure you have Guzzle 6. in your project: `composer require guzzlehttp/guzzle:"6."`. This package does not list Guzzle as a dependency because it is not strictly required (adapters for other HTTP clients are in the plans, and you can always write your own).
Use it!
Create a config object
Create a class in your application that extends Uncgits\CanvasApi\CanvasApiConfig
. Then, on __construct()
, utilize $this->setApiHost()
and $this->setToken()
methods to set up your API credentials. Most likely, you'll use environment variables here, or a reference to a non-committed class or file containing your credentials. Example:
<?php
namespace App\CanvasConfigs;
use Uncgits\CanvasApi\CanvasApiConfig;
class TestEnvironment extends CanvasApiConfig
{
public function __construct()
{
$this->setApiHost(getenv('CANVAS_DOMAIN'));
$this->setToken(getenv('CANVAS_TOKEN'));
}
}
Instantiate the CanvasApi
class
Choose the Client you want (found in
Uncgits\CanvasApi\Clients
), based on the type of call(s) you want to make.Then, choose the Adapter you want to use for the HTTP interaction (found in
Uncgits\CanvasApi\Adapters
).
By default, the
Guzzle
adapter is available with this package (You can write your own; more later).
Finally, choose the Config you want to use.
Instantiate the
Uncgits\CanvasApi\CanvasApi
class, and pass in the above three objects either using a constructor array, or the setter methods:
// OPTION 1 - array in constructor
// instantiate a client
$accountsClient = new \Uncgits\CanvasApi\Clients\Accounts;
// instantiate an adapter
$adapter = new \Uncgits\CanvasApi\Adapters\Guzzle;
// instantiate a config
$config = new App\CanvasConfigs\TestEnvironment;
// pass as array to API class
$api = new \Uncgits\CanvasApi\CanvasApi([
'client' => $accountsClient,
'adapter' => $adapter,
'config' => $config,
]);
// OPTION 2 - use setters
// instantiate a client
$accountsClient = new \Uncgits\CanvasApi\Clients\Accounts;
// instantiate an adapter
$adapter = new \Uncgits\CanvasApi\Adapters\Guzzle;
// instantiate a config
$config = new App\CanvasConfigs\TestEnvironment;
// instantiate API class
$api = new \Uncgits\CanvasApi\CanvasApi;
$api->setClient($accountsClient);
$api->setAdapter($adapter);
$api->setConfig($config);
Or, take the shortcut and instead pass the class names using either of the methods above:
// OPTION 1 - pass class names
use \Uncgits\CanvasApi\Clients\Accounts;
use \Uncgits\CanvasApi\Adapters\Guzzle;
use App\CanvasConfigs\TestEnvironment;
// instantiate the API class and pass in the array using class names
$api = new \Uncgits\CanvasApi\CanvasApi([
'client' => Accounts::class,
'adapter' => Guzzle::class,
'config' => TestEnvironment::class,
]);
// OPTION 2 - use setters
use \Uncgits\CanvasApi\Clients\Accounts;
use \Uncgits\CanvasApi\Adapters\Guzzle;
use App\CanvasConfigs\TestEnvironment;
$api = new \Uncgits\CanvasApi\CanvasApi([
'client' => Accounts::class,
'adapter' => Guzzle::class,
'config' => TestEnvironment::class,
]);
Then, once you have the client, if you want to list all Accounts:
// make the call
$result = $accountsClient->listAccounts(); // methods are named as they appear in Canvas API documentation
// get the contents of the result
var_dump($result->getContent()); // you receive a CanvasApiResult object
Client fluent shortcut
You may want to use a different Client while maintaining the same Adapter and Config. You can do this permanently by using the setClient()
on the API class, or you can do it for a single transaction using the fluent using()
chainable method.
using()
will accept a complete class name (with namespace) for a Client class, or a simplified class name for a built-in Client class, like 'users' or 'quizsubmissions'.
use Uncgits\CanvasApi\Clients\Users;
use \Uncgits\CanvasApi\Clients\Accounts;
use \Uncgits\CanvasApi\Adapters\Guzzle;
use App\CanvasConfigs\TestEnvironment;
$api = new \Uncgits\CanvasApi\CanvasApi([
'client' => Accounts::class,
'adapter' => Guzzle::class,
'config' => TestEnvironment::class,
]);
// API class will use Accounts client.
$api->listAccounts();
// use explicit setter - now API class will use Users client for all future calls
$api->setClient(Users::class);
$api->showUserDetails();
$api->getUserSettings(); // still Users client
// OR, use fluent setter with explicit class
$api->using(Users::class)->showUserDetails();
$api->listAccounts(); // back to original Accounts Client
// OR, fluent setter with implied class (from default library)
$api->using('users')->showUserDetails();
$api->listAccounts(); // back to Accounts Client
Setting parameters
Some API calls need additional parameters, and there are two different ways to pass them. The way(s) you choose are directly in line with the following logic:
If the parameter belongs in the URL, it will be required as an argument to the method call. Example:
getSingleAccount()
callsapi/v1/accounts/:id
where:id
is the ID of the Account you want to get. Therefore, the signature for the method isgetSingleAccount($id)
, and you would pass in your account value there.If the parameter is not contained in the URL, you need to set it as a body parameter. This includes all required parameters as well as optional parameters. Example:
listAccounts()
does not require any parameters, but you may optionally provide aninclude
parameter. Use the chainableaddParameters()
method on the client object like so:
$includes = [
'includes' => [
'lti_guid',
'registration_settings',
'services'
],
];
$result = $accountsClient->addParameters($includes)->listAccounts();
Refer to the API documentation for the accepted parameters, as well as the structure of the parameters (especially for multi-level nested parameters). This library attempts to validate parameters to a point, but as of initial release cannot validate some parameters that are deeply-nested.
Laravel wrapper
This package has a dedicated wrapper package for Laravel, that includes container bindings, facades, config files, adapters, and other utilities to help you use this package easily within a Laravel application.
Detailed Usage
Architecture
General Architecture
This library is comprised of many Clients, each of which interacts with one specific category / area of the Canvas API. For instance, there is a Quizzes client, an Accounts client, a Users client, and so on. Each client class is based fully on its architecture in the Canvas API Documentation. All client classes implement the Uncgits\CanvasApi\Clients\CanvasApiClientInterface
interface, which is currently an empty interface but helps to identify each Client as a "valid" Client (so you can build your own). The job of a Client is to return an array that can be parsed into a CanvasApiEndpoint
object, which requires an endpoint string, HTTP method string, and optional array of required parameters for the call to that Canvas API method.
Adapter classes are basically abstracted HTTP request handlers. They worry about structuring and making API calls to Canvas, handling pagination as necessary, and formatting the response in a simple, structured way (using a CanvasApiResult
object). These Adapter classes implement the Uncgits\CanvasApi\Adapters\CanvasApiAdapterInterface
interface. At the time of initial release, only an adapter for Guzzle is included - however, more will be added later, and you can always write your own to use your PHP HTTP library of choice. (Or straight cURL. Nobody's judging.) Adapters technically exist as properties on the Client class.
Config classes are classes that configure basic things that the adapter needs to know about in order to interact with the Canvas API. No concrete classes are included in this package - only the abstract CanvasApiConfig
class. The purpose for this architecture is so that you can create several classes to support different Canvas environments - even if you only interact with one Canvas domain, that domain has a test
and a beta
instance.
The Master API class is essentially the "traffic controller" class, that worries about making sure the separate components (Client, Adapter, Config) know everything they need to know in order to execute the API call. This class exists as such so that it can be extended as needed. Basically, when asked to make a call, the API class should know everything about what is required to make said API call, including the Client, Adapter, and Config that are being used, the endpoint and method, and so on, making it easy to leverage this class for informational purposes (such as logging or caching... see the Laravel wrapper package for an example).
Naming
Each client's methods are named as closely as possible to the names given to their corresponding operations in the API documentation. For example, the Accounts client contains listAccounts()
, listAccountsForCourseAdmins()
, and so on - all in line with the official documentation. The notable modification to this rule: 'a' / 'an' / 'the' are always dropped, so we have getSingleAccount()
, getTermsOfService()
, and so forth.
Aliasing
Where prudent, aliasing has been employed where the Canvas API does not always adhere to logical, RESTful semantic syntax. For instance, while getSingleAccount()
(as named via the Canvas API documentation) is descriptive, the 'single' modifier is not in line with how traditional RESTful transactions are named, and thus could be considered superfluous; in other words, many developers would guess this to be getAccount()
instead. So, in these cases, while the original core method will always adhere to its name in the official documentation, there are many places where aliases (like this example) have been added for convenience. These aliases simply call the "real" method and return its results.
Acting as a user
If you are using this library with a token that includes the permission to "Act As" (formerly "Masquerade" as) another user, you can utilize the chainable asUser()
method when making a call:
use Uncgits\CanvasApi\CanvasApi;
$api = new CanvasApi([
'config' = new \App\CanvasConfigs\TestEnvironment::class;
'adapter' = new \Uncgits\CanvasApi\Adapters\Guzzle::class;
'client' = new \Uncgits\CanvasApi\Clients\Users::class;
]);
$result = $api->asUser(12345)->listCourseNicknames();
SIS IDs
The Canvas REST API allows you to substitute a SIS ID in place of the internal Canvas ID for many items. SIS IDs are supported in this library!
Parameters passed as arguments to all methods are interpolated directly into the URL that is called. Thus, instead of calling $userClient->getUserSettings(12345)
you could instead call $userClient->getUserSettings('sis_login_id:jsmith')
, and it would work just fine. See the page above for where SIS IDs can and can't be substituted, and for details on how to handle encoding and escaping as needed.
Pagination
Pagination is handled automatically, where and when necessary, at the adapter level. Upon each API call the return headers are read, and the Link header is parsed and extracted, in accordance with the documentation.
To customize pagination on any API call, you may utilize addParameters()
to set the per_page
value to whatever you choose, or make use of the built-in helper setPerPage()
method:
$result = $api->addParameters(['per_page' => 100])->listAccounts();
// OR
$result = $api->setPerPage(100)->listAccounts();
Since the pagination headers are checked on every API call, each client transaction may therefore initiate several API calls before it reaches completion. See below for details on how results are presented.
Handling Results
Every time a client transaction is requested, this library will encapsulate important information and present it in the Uncgits\CanvasApi\CanvasApiResult
class. Within that class, you are able to access structured information on each API call made during that transaction, as well as an aggregated resultset that should be iterable as an array. For example, if you ask for all Accounts, and it requires 5 API calls of page size 10 to retrieve them, your result contents would be a single array of all accounts; this is designed to save you time in parsing them yourself!
$result = $api->listAccounts();
// get the result content - your Account objects
$accounts = $result->getContent();
// get a list of the API calls made
$apiCalls = $result->getCalls();
// get the first API call made
$firstCall = $result->getFirstCall();
// get the last API call made
$lastCall = $result->getLastCall();
// get the aggregate status code (success or failure) based on the result of all calls
$status = $result->getStatus();
// get the aggregate message (helpful if there were failures) based on the result of all calls
$status = $result->getMessage();
The API call array
Each API call (retrieved from the $calls
array on the CanvasApiResult
object) is made up of some key information that may be useful as you deal with things like throttling (see below), or other meta information on your calls. The API call array stores information on both the request (what is sent) and the response (what is received), and is structured as follows:
[
'request' => [
'endpoint' => $endpoint, // the final assembled endpoint
'method' => $method, // get, post, put, delete
'headers' => $requestOptions['headers'], // all headers passed - includes bearer info
'proxy' => $this->config->getProxy(), // proxy host and port, if using
'parameters' => $this->parameters, // any parameters used by the client
],
'response' => [
'headers' => $response->getHeaders(), // raw headers
'pagination' => $this->parse_http_rels($response->getHeaders()), // parsed pagination information
'code' => $response->getStatusCode(), // 200, 403, etc.
'reason' => $response->getReasonPhrase(), // OK, Forbidden, etc.
'runtime' => $response->getHeader('X-Runtime') ?? '', // convenience item for length of time in seconds the call ran
'cost' => $response->getHeader('X-Request-Cost') ?? '', // convenience item for "cost" of call toward rate limit
'rate-limit-remaining' => $response->getHeader('X-Rate-Limit-Remaining') ?? '', // convenience item for rate limit bucket level remaining
'body' => json_decode($response->getBody()->getContents()) // raw body content of the response
],
]
Rate limit / throttling
This library does not account for Rate Limit, Throttling, or automatic retries / exponential backoff. Most of the time, those issues are only encountered when running scripts that would invoke simultaneous API calls, and will not be a problem even when paginating through a large dataset. From Canvas API documentation:
Since the cost of a request is roughly based on the amount of time it takes to process, and the quota (by default) replenishes at a rate faster than real-time, any API client that makes no more than one simultaneous request is unlikely to be throttled.
This library is meant to act as an interface to the Canvas REST API; therefore it does not need to be concerned about how it is being used in client applications. You should take care to handle this in your application code - if rate limits are hit, you can expect this library to tell you by way of the response information on the individual API calls (code 403 with "Rate Limit Exceeded" message).
As a convenience, rate limit information is provided in the base level array of each API call result (see above), so that you do not need to parse through headers yourself.
Capping resultset size (Max Results)
By default the package ships with a somewhat-sensible default of 9999 maximum results per transaction. Generally speaking, if you're needing more records than this at a time, there is likely a better way to go about getting them (via search query parameters, etc.) than making 100+ paginated API calls. However, you're free to override this to the limits of your imagination (and system memory, and rate limit).
$config->setMaxResults(250);
// OR...
$api->using('users')->setMaxResults(250)->listUserPageViews('jdoe1'); // calling this on the API class will trickle through to the config class (assuming it is set)
Using a proxy
If you need to use an HTTP proxy, set that up in your CanvasApiConfig
object using setProxyHost()
, setProxyPort()
, and useProxy()
.
Passing additional headers
If you need to set additional headers on your requests, you can utilize the setAdditionalHeaders()
method on the client class, which accepts an array of key-value pairs.
File uploads (multipart data)
This library accommodates the POST method of uploading, as per Canvas documentation on file uploads. This process is broken into 2 (or 3) steps:
- The first API call will "inform" Canvas that you are going to upload a file, and tell it what the file will look like (type of file, desired filename, size, etc.)
- Canvas will pass back a URL to which the file itself will actually be sent, and a second API call will actually send the data via a multipart form request
- If the request results in a response code of
3xx
then a third call needs to be made to "finalize" the transaction.
Using this library, you will be able to upload files by doing the following:
- Calling the
uploadFile()
method in theFiles
client, and supplying thename
,size
, andcontent_type
parameters. - Inspecting the result to get the new POST URL for the file, as well as the
upload_params
. - Forming a valid multipart array from the
upload_params
, and appendingfile
. One example of a valid array using the included Guzzle adapter:
[
[
'name' => 'filename',
'contents' => 'myfile.jpg',
],
[
'name' => 'content_type',
'contents' => 'image/jpeg',
],
[
'name' => 'file',
'contents' => fopen('/path/to/file', 'r'),
],
]
Add the multipart data to the request and execute a call to
uploadFileToUploadUrl()
using the URL given to you.if you receive a
3xx
response code, follow the Location header and make a GET request to finalize the transaction. You can do this using theBase
client.
The process could look like:
use Uncgits\CanvasApi\CanvasApi;
$api = new CanvasApi([
'config' = new \App\CanvasConfigs\TestEnvironment::class;
'adapter' = new \Uncgits\CanvasApi\Adapters\Guzzle::class;
'client' = new \Uncgits\CanvasApi\Clients\Files::class;
]);
$result = $api->addParameters(['name' => 'myfile.jpg', 'size' => '100000', 'content_type' => 'image/jpeg']->uploadFile($folderId); // you may need to act-as the user here too.
// parse out the data
$url = $result->getContent()->upload_url;
$multipartArray = (array) $result->getContent()->upload_params;
$multipartArray['file'] = fopen('/path/to/file.jpg', 'r');
// create a valid multipart array for Guzzle
$multipartArray = array_map(function ($value, $key) {
return ['name' => $key, 'contents' => $value];
}, $multipartArray, array_keys($multipartArray));
// upload the file
$uploadResult = $api->addMultipart($multipartArray)->uploadFileToUploadUrl($url);
// finalize if needed
$code = $uploadResult->getLastResult()['code'];
if ($code >= 300 && $code < 400) {
$location = $uploadResult->getLastCall()['headers']['Location'];
$finalizeResult = $api->using('base')->makeCallToRawUrl($location, 'get');
}
Writing your own Adapters
An adapter is responsible for everything involved in the actual interaction with the Canvas API. The Adapter's responsibilities include:
- assembling the call endpoint, headers, parameters, body, and other options
- making the proper HTTP request
- parsing the response
- reading pagination headers and determining whether another call is necessary to fulfill the requested transaction
- reporting errors
- collating results into a single array
- recording all calls made in a transaction.
The CanvasApiAdapterInterface
interface shows how an adapter should be implemented. Most of the basic methods in that interface (setters, getters, convenience aliases, etc.) are implemented for you in the ExecutesApiCalls
trait, so generally speaking you should use that trait as a good first step. (Of course you can always override methods on the Trait if you prefer.)
On each adapter, therefore, that leaves you to implement the following methods on your own:
call()
transaction()
parsePagination()
normalizeResult()
Writing your own Clients
All Clients are implementations of the CanvasApiClientInterface
class, and passed in explicitly to the API object - and therefore writing your own Client classes is possible. There is no concrete implementation required by this interface class at this time...you can begin writing your custom Client anytime!.
Theoretically this API library will be complete at some point, and so writing custom Clients won't be necessary, but as this library is still growing it is possible that some API functionality is missing, and you'll want to fill gaps yourself. If you do, please consider creating a pull request to add your adapter to this repo!
Extending the API class
If you need additional functionality like logging or caching, you can write your own CanvasApi
wrapper class that extends this CanvasApi
class. The most common use cases would be to set some default values for the Config and Adapter classes, perhaps in the constructor, or to override the execute()
method to perform additional operations before the transaction is kicked off or after it is finished.
Contributing
Please feel free to submit a pull request to this package to help improve its coverage of the Canvas API, and/or to add Adapters or fix issues. The package was initially built to serve the needs of UNC Greensboro's immediate needs around Canvas integration, ad hoc, and is therefore not yet comprehensive.
License
See the LICENSE file for license rights and limitations (BSD).
Questions? Concerns?
Please use the Issue Tracker on this repository for reporting bugs. For security-related concerns, please contact its-laravel-devs-l@uncg.edu directly instead of using the Issue Tracker.
Version History
1.1.2
- Updates only the README file because we forgot to update it as part of Tag 1.1.1
1.1.1
- Updates the case of the "Link" header, to comply with changes made by Instructure.
- "Link" is now "link" in the Guzzle Adapter
- Change announced by Instructure here: Instructure Product Blog
1.1
- Fixes parameter ordering issue in
Users
client, which is deprecated in PHP 8 - Adjusts PHP version constraints specifically so that issues like this will be addressed in future major PHP version releases
1.0.3
- Adds handling in
CanvasApiResult
for the 'quota' key being handled as a single result
1.0.2
- Adds additional handling in
CanvasApiResult
for more single-member object results. This will likely need further examination but works for now. - Readme tweaks for clarity in installation / use
1.0.1
- Adds BSD license info to
composer.json
1.0
- Official open source licensing
0.6.7
- adds Quiz Submission Events client
- adds Feature Flags client
- adds special result handling for detecting a single feature flag
0.6.6
- change to PSR-4 declaration since we were already following it
0.6.5
- adds External Tool client
0.6.4
- actual bugfix for getting an error message back from the API.
0.6.3
- bugfix for getting an error message back from an API
0.6.2
- bugfixes for max results when only a single record is returned
0.6.1
- Adds missing call to get user profile (
Users
client)
0.6
- Adds true support for maximum results (capping resultset size).
0.5.2
- Tweaks the way that results are parsed, since some API calls return some strangely-organized entities - for instance, Enrollment Terms are wrapped in a parent object - so this is a low-hanging-fruit approach to sniff this out. If issues persist, we may need to be a bit more explicit and start writing result-parser classes for each Client.
0.5.1
- Adds ability to dynamically make call without Authorization / Bearer header
- Adds ability to dynamically tell Guzzle adapter to URL-encode the params (some POST requests need this)
0.5
- Support for multipart (file uploads). No helpers (yet) to formulate multipart or other data.
- Adds
getLastResult()
toCanvasApiResult
class, which outputs the code and reason of the last call.
0.4.3
- Adds
Groups
client
0.4.2
- Adds
reactivateEnrollment()
method toEnrollments
client
0.4.1
- Bugfixes for clients that were still referencing
$this->setParameters()
- Elimination of some helper methods that relied on
$this->setParameters()
- Some spelling fixes
0.4
- Yet another rewrite...centralizing everything back onto the API class, so that it is essentially all-knowing when a transaction is made.
- simplify
execute()
method for easy overwriting in wrapper classes - adds clients:
- SIS Imports
- SIS Import Errors
0.3
- Rewrite for new Client format
- Rework pagination bug in Guzzle adapter where pagination failed if other query parameters were present
using()
method is now temporary and does not overwrite original Client setting on API class- Removing requirement for setting Client, Adapter, and Config in order - instead putting in error checking along the way so that pieces can be swapped out more easily.
0.2
- Rewrite for new Adapter format
0.1
- First tagged release
- Supports the following operation sets fully:
- Account Reports
- Accounts
- Analytics
- Assignments
- Enrollments
- EnrollmentTerms
- Quiz Submissions
- Quizzes
- Roles
- Sections
- Tabs
- Supports the following operation sets nearly-fully (file uploads not yet included):
- Courses
- Users