jjuanrivvera/canvas-lms-kit

The most comprehensive PHP SDK for Canvas LMS API. Production-ready with +35 APIs implemented, rate limiting, and full test coverage.


README

Canvas LMS Kit

Canvas LMS Kit

Latest Version Total Downloads CI PHP Version License

The most comprehensive PHP SDK for Canvas LMS API. Production-ready with 37 APIs implemented.

โœจ Why Canvas LMS Kit?

  • ๐Ÿš€ Production Ready: Rate limiting, middleware support, battle-tested
  • ๐Ÿ“š Comprehensive: 37 Canvas APIs fully implemented
  • ๐Ÿ›ก๏ธ Type Safe: Full PHP 8.1+ type declarations and PHPStan level 6
  • ๐Ÿ”ง Developer Friendly: Intuitive Active Record pattern - just pass arrays!
  • ๐Ÿ“– Well Documented: Extensive examples, guides, and API reference
  • โšก Performance: Built-in pagination, caching support, and optimized queries

๐ŸŽฏ Quick Start

use CanvasLMS\Config;
use CanvasLMS\Api\Courses\Course;

Config::setApiKey('your-api-key');
Config::setBaseUrl('https://canvas.instructure.com');

// It's that simple!
$courses = Course::get();  // Get first page
foreach ($courses as $course) {
    echo $course->name . "\n";
}

๐Ÿ“‘ Table of Contents

๐Ÿ“‹ Requirements

  • PHP 8.1 or higher
  • Composer
  • Canvas LMS API token
  • Extensions: json, curl, mbstring

๐Ÿ“ฆ Installation

composer require jjuanrivvera/canvas-lms-kit

โš™๏ธ Configuration

API Key Authentication (Simple)

use CanvasLMS\Config;

// Basic configuration
Config::setApiKey('your-api-key');
Config::setBaseUrl('https://canvas.instructure.com');

// Optional: Set account ID for scoped operations
Config::setAccountId(1);

// Optional: Configure middleware
Config::setMiddleware([
    'retry' => ['max_attempts' => 3],
    'rate_limit' => ['wait_on_limit' => true],
]);

OAuth 2.0 Authentication (User-Based) ๐Ÿ†•

Canvas LMS Kit now supports OAuth 2.0 for user-specific authentication with automatic token refresh!

use CanvasLMS\Config;
use CanvasLMS\Auth\OAuth;

// Configure OAuth credentials
Config::setOAuthClientId('your-client-id');
Config::setOAuthClientSecret('your-client-secret');
Config::setOAuthRedirectUri('https://yourapp.com/oauth/callback');
Config::setBaseUrl('https://canvas.instructure.com');

// Step 1: Get authorization URL
$authUrl = OAuth::getAuthorizationUrl(['state' => 'random-state']);
// Redirect user to $authUrl

// Step 2: Handle callback
$tokenData = OAuth::exchangeCode($_GET['code']);

// Step 3: Use OAuth mode
Config::useOAuth();

// Now all API calls use OAuth with automatic token refresh!
$courses = Course::get(); // User's courses (first page)

Environment Variable Configuration

Perfect for containerized deployments and 12-factor apps:

# .env file
CANVAS_BASE_URL=https://canvas.instructure.com
CANVAS_API_KEY=your-api-key
# OR for OAuth:
CANVAS_OAUTH_CLIENT_ID=your-client-id
CANVAS_OAUTH_CLIENT_SECRET=your-client-secret
CANVAS_AUTH_MODE=oauth
// Auto-detect from environment
Config::autoDetectFromEnvironment();
// Ready to use!

Logging Configuration

The SDK supports any PSR-3 compatible logger for comprehensive debugging and monitoring:

use CanvasLMS\Config;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// Configure with Monolog
$logger = new Logger('canvas-lms');
$logger->pushHandler(new StreamHandler('logs/canvas.log', Logger::INFO));
Config::setLogger($logger);

// All API calls, OAuth operations, pagination, and file uploads are now logged!

Symfony Integration

use Psr\Log\LoggerInterface;

public function __construct(LoggerInterface $logger) {
    Config::setLogger($logger);
}

Advanced Logging Configuration

// Enable detailed request/response logging
Config::setMiddleware([
    'logging' => [
        'enabled' => true,
        'log_requests' => true,
        'log_responses' => true,
        'log_errors' => true,
        'log_timing' => true,
        'log_level' => \Psr\Log\LogLevel::DEBUG,
        'sanitize_fields' => ['password', 'token', 'api_key', 'secret'],
        'max_body_length' => 1000
    ]
]);

What Gets Logged:

  • โœ… All API requests and responses (with sensitive data sanitization)
  • โœ… OAuth token operations (refresh, revoke, exchange)
  • โœ… Pagination operations with performance metrics
  • โœ… File upload progress (3-step process)
  • โœ… Rate limit headers and API costs
  • โœ… Error conditions with context

Security: The logging middleware automatically sanitizes sensitive fields like passwords, tokens, and API keys to prevent accidental exposure in logs.

๐Ÿ’ก Usage Examples

Working with Courses

use CanvasLMS\Api\Courses\Course;

// Get first page of courses (memory efficient)
$courses = Course::get();

// Get ALL courses across all pages automatically
// โš ๏ธ Warning: Be cautious with large datasets (1000+ items)
$allCourses = Course::all();

// Get paginated results with metadata (recommended for large datasets)
$paginated = Course::paginate(['per_page' => 50]);
echo "Total courses: " . $paginated->getTotalCount();

// Find a specific course
$course = Course::find(123);

// Create a new course - just pass an array!
$course = Course::create([
    'name' => 'Introduction to PHP',
    'course_code' => 'PHP101',
    'start_at' => '2025-02-01T00:00:00Z'
]);

// Update a course
$course->update([
    'name' => 'Advanced PHP Programming'
]);

// Save changes with fluent interface support
$course->name = 'Updated Course Name';
$course->save()->enrollments(); // Save and immediately get enrollments

// Delete a course (also returns self for chaining)
$course->delete();

Managing Assignments

use CanvasLMS\Api\Assignments\Assignment;
use CanvasLMS\Api\Submissions\Submission;
use CanvasLMS\Api\Courses\Course;

// Set course context for Assignment
$course = Course::find(123);
Assignment::setCourse($course);

// Create an assignment - simple array syntax
$assignment = Assignment::create([
    'name' => 'Final Project',
    'description' => 'Build a web application',
    'points_possible' => 100,
    'due_at' => '2025-03-15T23:59:59Z',
    'submission_types' => ['online_upload', 'online_url']
]);

// Grade submissions (requires both Course and Assignment context)
Submission::setCourse($course);
Submission::setAssignment($assignment);

$submission = Submission::update($studentId, [
    'posted_grade' => 95,
    'comment' => 'Excellent work!'
]);

Working with Modules and Module Items

use CanvasLMS\Api\Modules\Module;
use CanvasLMS\Api\Modules\ModuleItem;

// Get course and set context
$course = Course::find(123);
Module::setCourse($course);

// Create a module
$module = Module::create([
    'name' => 'Week 1: Introduction',
    'position' => 1,
    'published' => true
]);

// Add items to the module (requires both course and module context)
ModuleItem::setCourse($course);
ModuleItem::setModule($module);

// Add an assignment to the module
$item = ModuleItem::create([
    'title' => 'Week 1 Assignment',
    'type' => 'Assignment',
    'content_id' => $assignment->id,
    'position' => 1,
    'completion_requirement' => [
        'type' => 'must_submit'
    ]
]);

// Or use the course instance method (recommended)
$modules = $course->modules();

Working with Current User

use CanvasLMS\Api\Users\User;

// Get current authenticated user instance
$currentUser = User::self();

// Canvas supports 'self' for these endpoints:
$profile = $currentUser->getProfile();
$activityStream = $currentUser->getActivityStream();
$todos = $currentUser->getTodoItems();
$groups = $currentUser->groups();

// Other methods require explicit user ID
$user = User::find(123);
$enrollments = $user->enrollments();
$courses = $user->courses();

Managing Groups

use CanvasLMS\Api\Groups\Group;
use CanvasLMS\Api\Groups\GroupMembership;

// Create a group in your account
$group = Group::create([
    'name' => 'Study Group Alpha',
    'description' => 'Weekly study sessions',
    'is_public' => false,
    'join_level' => 'invitation_only'
]);

// Add members to the group
$membership = $group->createMembership([
    'user_id' => 456,
    'workflow_state' => 'accepted'
]);

// Get group activity stream
$activities = $group->activityStream();

// Invite users by email
$group->invite(['student1@example.com', 'student2@example.com']);

File Uploads

use CanvasLMS\Api\Files\File;

// Upload a file to a course
$file = File::upload([
    'course_id' => 123,
    'file_path' => '/path/to/document.pdf',
    'name' => 'Course Syllabus.pdf',
    'parent_folder_path' => 'course_documents'
]);

Feature Flags

use CanvasLMS\Api\FeatureFlags\FeatureFlag;
use CanvasLMS\Api\Courses\Course;

// Get feature flags for the account
$flags = FeatureFlag::get();     // First page
$allFlags = FeatureFlag::all();  // All flags

// Get a specific feature flag
$flag = FeatureFlag::find('new_gradebook');

// Enable a feature for the account
$flag->enable();

// Disable a feature
$flag->disable();

// Feature flags for a specific course
$course = Course::find(123);
$courseFlags = $course->featureFlags();

// Enable a feature for a specific course
$courseFlag = $course->getFeatureFlag('anonymous_marking');
$courseFlag->enable();

Conversations (Internal Messaging)

use CanvasLMS\Api\Conversations\Conversation;

// Get conversations for the current user
$conversations = Conversation::get(['scope' => 'unread']); // First page
$allConversations = Conversation::all(['scope' => 'unread']); // All pages

// Create a new conversation
$conversation = Conversation::create([
    'recipients' => ['user_123', 'course_456_students'],
    'subject' => 'Assignment Feedback',
    'body' => 'Great work on your recent submission!',
    'group_conversation' => true
]);

// Add a message to existing conversation
$conversation->addMessage([
    'body' => 'Following up on my previous message...',
    'attachment_ids' => [789] // Attach files
]);

// Manage conversation state
$conversation->markAsRead();
$conversation->star();
$conversation->archive();

// Batch operations
Conversation::batchUpdate([1, 2, 3], ['event' => 'mark_as_read']);
Conversation::markAllAsRead();

// Get unread count
$unreadCount = Conversation::getUnreadCount();

Working with Multi-Context Resources

Some Canvas resources exist in multiple contexts (Account, Course, User, Group). Canvas LMS Kit follows an Account-as-Default convention for consistency:

use CanvasLMS\Api\Rubrics\Rubric;
use CanvasLMS\Api\ExternalTools\ExternalTool;
use CanvasLMS\Api\CalendarEvents\CalendarEvent;
use CanvasLMS\Api\Files\File;

// Direct calls default to Account context
$rubrics = Rubric::get();           // Account-level rubrics (first page)
$tools = ExternalTool::get();       // Account-level external tools (first page)
$events = CalendarEvent::get();     // Account-level calendar events (first page)
$files = File::get();               // Exception: User files (no account context)

// Course-specific access through Course instance
$course = Course::find(123);
$courseRubrics = $course->rubrics();            // Course-specific rubrics
$courseTools = $course->externalTools();        // Course-specific tools
$courseEvents = $course->calendarEvents();      // Course-specific events
$courseFiles = $course->files();                // Course-specific files

// Direct context access when needed
$userEvents = CalendarEvent::fetchByContext('user', 456);
$groupFiles = File::fetchByContext('groups', 789);

Multi-Context Resources:

  • Rubrics (Account/Course)
  • External Tools (Account/Course)
  • Calendar Events (Account/Course/User/Group)
  • Files (User/Course/Group) - No Account context
  • Groups (Account/Course/User)
  • Content Migrations (Account/Course/Group/User)

Performance Considerations & Memory Usage

When working with large Canvas instances (universities, enterprise organizations), be mindful of memory usage:

// โœ… GOOD: Memory-efficient for large datasets
$result = User::paginate(['per_page' => 100]);
while ($result) {
    foreach ($result->getData() as $user) {
        // Process batch of 100 users
    }
    $result = $result->hasNextPage() ? $result->getNextPage() : null;
}

// โœ… GOOD: Get only what you need
$recentCourses = Course::get(['per_page' => 20]); // Just first 20

// โš ๏ธ CAUTION: Loads entire dataset into memory
$allUsers = User::all();  // Could be 50,000+ users!
$allEnrollments = Enrollment::all(); // Could be millions!

// โš ๏ธ BETTER: Process in batches for large datasets
$page = 1;
do {
    $batch = Enrollment::paginate(['page' => $page++, 'per_page' => 500]);
    // Process batch
} while ($batch->hasNextPage());

Memory Guidelines:

  • Use get() for dashboards and quick views (1 API call)
  • Use paginate() for large datasets and UI tables (controlled memory)
  • Use all() only when you need complete data AND know it's reasonably sized
  • Consider your server's memory limit when using all() on production data

Working with Course-Scoped Resources

Some Canvas resources are strictly course-scoped and require setting the course context before use:

use CanvasLMS\Api\Pages\Page;
use CanvasLMS\Api\Quizzes\Quiz;
use CanvasLMS\Api\Modules\Module;
use CanvasLMS\Api\DiscussionTopics\DiscussionTopic;
use CanvasLMS\Api\Courses\Course;

// Get your course
$course = Course::find(123);

// Option 1: Set context for each API (required for direct API calls)
Page::setCourse($course);
Quiz::setCourse($course);
Module::setCourse($course);
DiscussionTopic::setCourse($course);

// Now you can use the APIs directly
$pages = Page::get();         // Get first page
$quizzes = Quiz::all();       // Get all quizzes
$modules = Module::get();     // Get first page
$discussions = DiscussionTopic::all(); // Get all discussions

// Option 2: Use course instance methods (recommended - no context setup needed)
$pages = $course->pages();          // Returns first page only
$quizzes = $course->quizzes();      // Returns first page only
$modules = $course->modules();      // Returns first page only
$discussions = $course->discussionTopics(); // Returns first page only

Important Notes:

  1. These APIs will throw an exception if you try to use them without setting the course context first.
  2. Relationship methods return FIRST PAGE ONLY for performance. To get all items:
    // Get ALL modules for a course
    Module::setCourse($course);
    $allModules = Module::all();  // Gets all pages
    
    // Or paginate for control
    $paginated = Module::paginate(['per_page' => 50]);

Learning Outcomes

use CanvasLMS\Api\Outcomes\Outcome;
use CanvasLMS\Api\OutcomeGroups\OutcomeGroup;

// Create an outcome group
$group = OutcomeGroup::create([
    'title' => 'Critical Thinking Skills',
    'description' => 'Core competencies for analytical thinking'
]);

// Create a learning outcome
$outcome = Outcome::create([
    'title' => 'Analyze Complex Problems',
    'description' => 'Student can break down complex problems into manageable parts',
    'mastery_points' => 3,
    'ratings' => [
        ['description' => 'Exceeds', 'points' => 4],
        ['description' => 'Mastery', 'points' => 3],
        ['description' => 'Near Mastery', 'points' => 2],
        ['description' => 'Below Mastery', 'points' => 1]
    ]
]);

// Link outcome to a group
$group->linkOutcome($outcome->id);

// Align outcome with an assignment
$assignment = Assignment::find(123);
$assignment->alignOutcome($outcome->id, [
    'mastery_score' => 3,
    'possible_score' => 4
]);

Content Migrations

use CanvasLMS\Api\Courses\Course;
use CanvasLMS\Api\ContentMigrations\ContentMigration;

// Copy content between courses
$course = Course::find(456);
$migration = $course->copyContentFrom(123, [
    'except' => ['announcements', 'calendar_events']
]);

// Selective copy with specific items
$migration = $course->selectiveCopyFrom(123, [
    'assignments' => [1, 2, 3],
    'quizzes' => ['quiz-1', 'quiz-2'],
    'modules' => [10, 11]
]);

// Import a Common Cartridge file
$migration = $course->importCommonCartridge('/path/to/course.imscc');

// Copy with date shifting
$migration = $course->copyWithDateShift(123, '2024-01-01', '2025-01-01', [
    'shift_dates' => true,
    'old_start_date' => '2024-01-01',
    'new_start_date' => '2025-01-01'
]);

// Track migration progress
while (!$migration->isCompleted()) {
    $progress = $migration->getProgress();
    echo "Migration {$progress->workflow_state}: {$progress->completion}%\n";
    sleep(5);
    $migration->refresh();
}

// Handle migration issues
$issues = $migration->migrationIssues();
foreach ($issues as $issue) {
    if ($issue->workflow_state === 'active') {
        $issue->resolve();
    }
}

๐Ÿ“Š Supported APIs

โœ… Currently Implemented (37 APIs)

๐Ÿ“š Core Course Management
  • โœ… Courses - Full CRUD operations
  • โœ… Modules - Content organization
  • โœ… Module Items - Individual content items
  • โœ… Sections - Course sections
  • โœ… Tabs - Navigation customization
  • โœ… Pages - Wiki-style content
๐Ÿ‘ฅ Users & Enrollment
  • โœ… Users - User management with self() pattern support
  • โœ… Enrollments - Course enrollments
  • โœ… Admins - Administrative roles
  • โœ… Accounts - Account management
๐Ÿ“ Assessment & Grading
  • โœ… Assignments - Assignment management
  • โœ… Quizzes - Quiz creation and management
  • โœ… Quiz Submissions - Student attempts
  • โœ… Submissions - Assignment submissions
  • โœ… Submission Comments - Feedback
  • โœ… Rubrics - Grading criteria and assessment
  • โœ… Rubric Associations - Link rubrics to assignments
  • โœ… Rubric Assessments - Grade with rubrics
  • โœ… Outcomes - Learning objectives and competencies
  • โœ… Outcome Groups - Organize learning outcomes
  • โœ… Outcome Imports - Import outcome data
  • โœ… Outcome Results - Track student achievement
๐Ÿ’ฌ Communication & Collaboration
  • โœ… Discussion Topics - Forums and discussions
  • โœ… Groups - Student groups and collaboration
  • โœ… Group Categories - Organize and manage groups
  • โœ… Group Memberships - Group member management
  • โœ… Conferences - Web conferencing integration
  • โœ… Conversations - Internal messaging system
  • ๐Ÿ”„ Announcements - Course announcements (coming soon)
๐Ÿ”ง Tools & Integration
  • โœ… Files - File management and uploads
  • โœ… External Tools - LTI integrations
  • โœ… Module Assignment Overrides - Custom dates
  • โœ… Calendar Events - Event management
  • โœ… Appointment Groups - Scheduling
  • โœ… Progress - Async operation tracking
  • โœ… Content Migrations - Import/export course content
  • โœ… Migration Issues - Handle import problems
  • โœ… Feature Flags - Manage Canvas feature toggles

๐Ÿš€ Advanced Features

Production-Ready Middleware

// The SDK automatically handles rate limiting and retries
$course = Course::find(123); // Protected by middleware

// Canvas API Rate Limiting (3000 requests/hour)
// โœ… Automatic throttling when approaching limits
// โœ… Smart backoff strategies
// โœ… Transparent to your application

Multi-Tenant Support

// Manage multiple Canvas instances
Config::setContext('production');
$prodCourse = Course::find(123);

Config::setContext('test');
$testCourse = Course::find(456);

Pagination Support (Simplified API)

// Three simple methods for all your pagination needs:

// 1. get() - Fetch first page only (fast, memory efficient)
$courses = Course::get(['per_page' => 100]); 

// 2. all() - Fetch ALL items across all pages automatically
$allCourses = Course::all();

// 3. paginate() - Get results with pagination metadata
$result = Course::paginate(['per_page' => 50]);
echo "Page {$result->getCurrentPage()} of {$result->getTotalPages()}";
echo "Total courses: {$result->getTotalCount()}";

// Access the data
foreach ($result->getData() as $course) {
    echo $course->name;
}

// Navigate pages
if ($result->hasNextPage()) {
    $nextPage = $result->getNextPage();
}

Fluent Interface & Method Chaining

All save() and delete() methods return the instance, enabling method chaining:

// Save and continue working with the object
$course->name = 'New Name';
$enrollments = $course->save()->enrollments();

// Chain multiple operations
$assignment = new Assignment();
$assignment->name = 'Final Project';
$assignment->points_possible = 100;
$submissions = $assignment->save()->getSubmissions();

// Error handling with fluent interface
try {
    $user->email = 'new@example.com';
    $user->save()->enrollments(); // Save and get enrollments
} catch (CanvasApiException $e) {
    // Handle error - no more silent failures!
}

Relationship Methods

// Efficient relationship loading
$course = Course::find(123);
$students = $course->getStudents();
$assignments = $course->assignments();
$modules = $course->modules();

Context Management (Account-as-Default)

Canvas LMS Kit uses the Account-as-Default convention for multi-context resources:

// Direct API calls use Account context (Config::getAccountId())
$groups = Group::get();              // First page of groups in the account
$allGroups = Group::all();           // All groups in the account
$rubrics = Rubric::get();            // First page of rubrics in the account
$migrations = ContentMigration::all(); // All migrations in the account

// Course-specific access via Course instance methods
$course = Course::find(123);
$courseGroups = $course->groups();              // Groups in this course
$courseRubrics = $course->rubrics();            // Rubrics in this course
$courseMigrations = $course->contentMigrations(); // Migrations in this course

// User-specific access via User instance methods
$user = User::find(456);
$userGroups = $user->groups();                    // User's groups
$userMigrations = $user->contentMigrations();    // User's migrations

// Group-specific access via Group instance methods
$group = Group::find(789);
$groupMigrations = $group->contentMigrations();  // Group's migrations

Why Account-as-Default?

  • โœ… Consistency across all multi-context resources
  • โœ… Respects Canvas hierarchy (Account โ†’ Course โ†’ User/Group)
  • โœ… Clean separation of concerns
  • โœ… No confusion about which context is being used

๐Ÿ“– Full Context Management Guide

๐Ÿงช Testing

# Using Docker (recommended)
docker compose exec php composer test
docker compose exec php composer check  # Run all checks

# Local development
composer test
composer cs-fix   # Fix coding standards
composer phpstan  # Static analysis

๐Ÿค Contributing

We welcome contributions! Please see our Contributing Guidelines.

Quick Contribution Guide

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (composer check)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ’ฌ Support

Report a Bug Request Feature Ask Question

Resources

โญ Show Your Support

If you find this project helpful, please consider giving it a star on GitHub! It helps others discover the project and motivates continued development.

Star History Chart

Built with โค๏ธ by the Canvas LMS Kit community