From 88434683bdcc0861ab61f3d8da2b70b8f847ed45 Mon Sep 17 00:00:00 2001 From: Thomas Wijnands Date: Fri, 27 Oct 2023 14:19:47 +0200 Subject: [PATCH] feat: add lti provider build with laravel eloquent --- CHANGELOG.md | 21 +- CODE_OF_CONDUCT.md | 74 +++ CONTRIBUTING.md | 32 + ISSUE_TEMPLATE.md | 27 + LICENSE.md | 38 +- PULL_REQUEST_TEMPLATE.md | 43 ++ README.md | 114 ++-- composer.json | 54 +- database/factories/ModelFactory.php | 19 - ...10_26_100000_add_client_and_lti_tables.php | 130 ++++ .../migrations/create_skeleton_table.php.stub | 19 - phpstan.neon.dist | 10 +- phpunit.xml.dist | 2 +- src/Commands/SkeletonCommand.php | 19 - src/Facades/Skeleton.php | 16 - src/LtiServiceProvider.php | 24 + src/ModelDataConnector.php | 553 ++++++++++++++++++ src/Models/Contracts/LtiClient.php | 40 ++ src/Models/Contracts/LtiEnvironment.php | 33 ++ src/Models/LtiAccessToken.php | 92 +++ src/Models/LtiContext.php | 105 ++++ src/Models/LtiNonce.php | 79 +++ src/Models/LtiResourceLink.php | 130 ++++ src/Models/LtiUserResult.php | 75 +++ src/Models/Traits/HasLtiEnvironment.php | 18 + src/Models/Traits/IsLtiEnvironment.php | 55 ++ src/Skeleton.php | 7 - src/SkeletonServiceProvider.php | 25 - tests/Pest.php | 2 +- tests/TestCase.php | 10 +- 30 files changed, 1629 insertions(+), 237 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md create mode 100644 PULL_REQUEST_TEMPLATE.md delete mode 100644 database/factories/ModelFactory.php create mode 100644 database/migration/2023_10_26_100000_add_client_and_lti_tables.php delete mode 100644 database/migrations/create_skeleton_table.php.stub delete mode 100644 src/Commands/SkeletonCommand.php delete mode 100644 src/Facades/Skeleton.php create mode 100644 src/LtiServiceProvider.php create mode 100644 src/ModelDataConnector.php create mode 100644 src/Models/Contracts/LtiClient.php create mode 100644 src/Models/Contracts/LtiEnvironment.php create mode 100644 src/Models/LtiAccessToken.php create mode 100644 src/Models/LtiContext.php create mode 100644 src/Models/LtiNonce.php create mode 100644 src/Models/LtiResourceLink.php create mode 100644 src/Models/LtiUserResult.php create mode 100644 src/Models/Traits/HasLtiEnvironment.php create mode 100644 src/Models/Traits/IsLtiEnvironment.php delete mode 100755 src/Skeleton.php delete mode 100644 src/SkeletonServiceProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b3242..ea658b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ # Changelog -All notable changes to `:package_name` will be documented in this file. +All notable changes to `laravel-elastic` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. + +## NEXT - YYYY-MM-DD + +### Added +- Nothing + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..73453b7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at `service@swis.nl`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7b6b087 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/swisnl/laravel-elastic). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5b48c57 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Detailed description + +Provide a detailed description of the change or addition you are proposing. + +Make it clear if the issue is a bug, an enhancement or just a question. + +## Context + +Why is this change important to you? How would you use it? + +How can it benefit other users? + +## Possible implementation + +Not obligatory, but suggest an idea for implementing addition or change. + +## Your environment + +Include as many relevant details about the environment you experienced the bug in and how to reproduce it. + +* Version used (e.g. PHP 5.6, HHVM 3): +* Operating system and version (e.g. Ubuntu 16.04, Windows 7): +* Link to your project: +* ... +* ... diff --git a/LICENSE.md b/LICENSE.md index 58c9ad4..bfa1977 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,21 @@ -The MIT License (MIT) +# The MIT License (MIT) -Copyright (c) :vendor_name +Copyright (c) 2023 SWIS BV -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..86246b3 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ + + +## Description + +Describe your changes in detail. + +## Motivation and context + +Why is this change required? What problem does it solve? + +If it fixes an open issue, please link to the issue here (if you write `fixes #num` +or `closes #num`, the issue will be automatically closed when the pull is accepted.) + +## How has this been tested? + +Please describe in detail how you tested your changes. + +Include details of your testing environment, and the tests you ran to +see how your change affects other areas of the code, etc. + +## Screenshots (if appropriate) + +## Types of changes + +What types of changes does your code introduce? Put an `x` in all the boxes that apply: +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + +Go over all the following points, and put an `x` in all the boxes that apply. + +Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). + +- [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. +- [ ] My pull request addresses exactly one patch/feature. +- [ ] I have created a branch for this patch/feature. +- [ ] Each individual commit in the pull request is meaningful. +- [ ] I have added tests to cover my changes. +- [ ] If my change requires a change to the documentation, I have updated it accordingly. + +If you're unsure about any of these, don't hesitate to ask. We're here to help! diff --git a/README.md b/README.md index 375da96..caf939f 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,75 @@ -# :package_description +# laravel-elastic -[![Latest Version on Packagist](https://img.shields.io/packagist/v/:vendor_slug/:package_slug.svg?style=flat-square)](https://packagist.org/packages/:vendor_slug/:package_slug) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/:vendor_slug/:package_slug/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/:vendor_slug/:package_slug/actions?query=workflow%3Arun-tests+branch%3Amain) -[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/:vendor_slug/:package_slug/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/:vendor_slug/:package_slug/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) -[![Total Downloads](https://img.shields.io/packagist/dt/:vendor_slug/:package_slug.svg?style=flat-square)](https://packagist.org/packages/:vendor_slug/:package_slug) - ---- -This repo can be used to scaffold a Laravel package. Follow these steps to get started: +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Buy us a tree][ico-treeware]][link-treeware] +[![Build Status][ico-github-actions]][link-github-actions] +[![Total Downloads][ico-downloads]][link-downloads] +[![Made by SWIS][ico-swis]][link-swis] -1. Press the "Use this template" button at the top of this repo to create a new repo with the contents of this skeleton. -2. Run "php ./configure.php" to run a script that will replace all placeholders throughout all the files. -3. Have fun creating your package. -4. If you need help creating a package, consider picking up our Laravel Package Training video course. ---- - -This is where your description should go. Limit it to a paragraph or two. Consider adding a small example. -## Support us +Laravel package that implements elasticsearch in an easy way. Index various models via the `SyncsWithIndex` trait and customize further by extending the `Document` and `SearchResult` class. -[](https://spatie.be/github-ad-click/:package_name) -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). +## Install -## Installation +Via Composer -You can install the package via composer: - -```bash -composer require :vendor_slug/:package_slug -``` - -You can publish and run the migrations with: - -```bash -php artisan vendor:publish --tag=":package_slug-migrations" -php artisan migrate -``` - -You can publish the config file with: - -```bash -php artisan vendor:publish --tag=":package_slug-config" -``` - -This is the contents of the published config file: - -```php -return [ -]; -``` - -Optionally, you can publish the views using - -```bash -php artisan vendor:publish --tag=":package_slug-views" +``` bash +$ composer require swisnl/laravel-elasticsearch ``` - ## Usage - -```php -$variable = new VendorName\Skeleton(); -echo $variable->echoPhrase('Hello, VendorName!'); +implemnent the LtiClient interface to your oauth2 client model. Implement the methods and add the following attributes to the model ``` - -## Testing - -```bash -composer test +'lti_platform_id', +'lti_client_id', +'lti_deployment_id', +'lti_version', +'lti_signature_method', +'lti_profile', +'lti_settings', +'lti_user_type' ``` -## Changelog +## Change log Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. -## Security Vulnerabilities +## Security -Please review [our security policy](../../security/policy) on how to report security vulnerabilities. +If you discover any security related issues, please email security@swis.nl instead of using the issue tracker. ## Credits -- [:author_name](https://github.com/:author_username) -- [All Contributors](../../contributors) +- [Thomas Wijnands][link-author] +- [All Contributors][link-contributors] ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**][link-treeware] to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats. + +## SWIS :heart: Open Source + +[SWIS][link-swis] is a web agency from Leiden, the Netherlands. We love working with open source software. + +[ico-version]: https://img.shields.io/packagist/v/swisnl/laravel-elasticsearch.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-treeware]: https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen.svg?style=flat-square +[ico-github-actions]: https://img.shields.io/github/actions/workflow/status/swisnl/laravel-elasticsearch/tests.yml?label=tests&branch=master&style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/swisnl/laravel-elasticsearch.svg?style=flat-square +[ico-swis]: https://img.shields.io/badge/%F0%9F%9A%80-made%20by%20SWIS-%230737A9.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/swisnl/laravel-elasticsearch +[link-github-actions]: https://github.com/swisnl/laravel-elasticsearch/actions/workflows/tests.yml +[link-downloads]: https://packagist.org/packages/swisnl/laravel-elasticsearch +[link-treeware]: https://plant.treeware.earth/swisnl/laravel-elasticsearch +[link-author]: https://github.com/tommie1001 +[link-contributors]: ../../contributors +[link-swis]: https://www.swis.nl diff --git a/composer.json b/composer.json index c8ba78b..ba0cfd1 100644 --- a/composer.json +++ b/composer.json @@ -1,24 +1,27 @@ { - "name": ":vendor_slug/:package_slug", - "description": ":package_description", + "name": "swisnl/laravel-lti-provider", + "type": "library", + "description": "Laravel lti provider", "keywords": [ - ":vendor_name", - "laravel", - ":package_slug" + "swisnl", + "laravel-lti" ], - "homepage": "https://github.com/:vendor_slug/:package_slug", + "homepage": "https://github.com/swisnl/laravel-lti-provider", "license": "MIT", "authors": [ { - "name": ":author_name", - "email": "author@domain.com", + "name": "Thomas Wijnands", + "email": "twijnands@swis.nl", + "homepage": "https://www.swis.nl", "role": "Developer" } ], "require": { "php": "^8.1", - "spatie/laravel-package-tools": "^1.14.0", - "illuminate/contracts": "^10.0" + "laravel/framework": "^10.0", + "spatie/laravel-package-tools": "^1.15", + "illuminate/contracts": "^10.0", + "celtic/lti": "^5.0" }, "require-dev": { "laravel/pint": "^1.0", @@ -35,14 +38,7 @@ }, "autoload": { "psr-4": { - "VendorName\\Skeleton\\": "src/", - "VendorName\\Skeleton\\Database\\Factories\\": "database/factories/" - } - }, - "autoload-dev": { - "psr-4": { - "VendorName\\Skeleton\\Tests\\": "tests/", - "Workbench\\App\\": "workbench/app/" + "Swis\\Laravel\\LtiProvider\\": "src" } }, "scripts": { @@ -63,21 +59,19 @@ "test-coverage": "vendor/bin/pest --coverage", "format": "vendor/bin/pint" }, - "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true, - "phpstan/extension-installer": true - } - }, "extra": { "laravel": { "providers": [ - "VendorName\\Skeleton\\SkeletonServiceProvider" - ], - "aliases": { - "Skeleton": "VendorName\\Skeleton\\Facades\\Skeleton" - } + "Swis\\Laravel\\LtiProvider\\LtiServiceProvider" + ] + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true, + "phpstan/extension-installer": true, + "pestphp/pest-plugin": true } }, "minimum-stability": "dev", diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index c51604f..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -uuid('id')->primary(); + $table->integer('nr')->unique(); + + // Admin title + $table->string('name'); + + // Keys & secrets + $table->string('secret', 1024)->nullable(); + $table->text('public_key')->nullable(); + + // Branding + $table->text('redirect'); + $table->string('home_url')->nullable(); + $table->string('logo')->nullable(); + + // Policies + $table->boolean('revoked')->default(false); + + // LTI information + $table->string('lti_platform_id', 255)->nullable(); + $table->string('lti_client_id', 255)->nullable(); + $table->string('lti_deployment_id', 255)->nullable(); + $table->string('lti_version', 10)->nullable(); + $table->string('lti_signature_method', 15)->default('HMAC-SHA1'); + $table->text('lti_profile'); + $table->text('lti_settings'); + $table->string('lti_user_type')->default('external_user')->after('lti_settings'); + + $table->timestamps(); + + $table->unique(['lti_platform_id', 'lti_client_id', 'lti_deployment_id']); + }); + + Schema::create('lti_nonces', function (Blueprint $table) { + $table->uuid('id')->primary(); + + $table->uuidMorphs('lti_environment'); + + $table->foreignUuid('client_id')->constrained('clients')->cascadeOnDelete(); + $table->string('nonce', 50); + $table->dateTime('expires_at'); + + $table->timestamps(); + + $table->unique(['client_id', 'nonce']); + }); + + Schema::create('lti_access_tokens', function (Blueprint $table) { + $table->uuid('id')->primary(); + + $table->uuidMorphs('lti_environment'); + + $table->foreignUuid('client_id')->unique()->constrained('clients')->cascadeOnDelete(); + + $table->string('access_token', 2000); + $table->text('scopes'); + $table->dateTime('expires_at'); + + $table->timestamps(); + }); + + Schema::create('lti_contexts', function (Blueprint $table) { + $table->id(); + + $table->uuidMorphs('lti_environment'); + + $table->foreignUuid('client_id')->constrained('clients')->cascadeOnDelete(); + $table->string('title')->nullable(); + $table->string('external_context_id', 255); + $table->text('settings'); + + $table->timestamps(); + }); + + Schema::create('lti_resource_links', function (Blueprint $table) { + $table->id(); + + $table->uuidMorphs('lti_environment'); + + $table->foreignUuid('client_id')->nullable()->constrained('clients')->cascadeOnDelete(); + $table->foreignId('lti_context_id')->nullable()->constrained('lti_contexts')->cascadeOnDelete(); + + $table->string('title')->nullable(); + $table->string('external_resource_link_id', 255); + $table->text('settings'); + + $table->timestamps(); + }); + + Schema::create('lti_user_results', function (Blueprint $table) { + $table->id(); + + $table->uuidMorphs('lti_environment'); + + $table->foreignId('lti_resource_link_id')->constrained('lti_resource_links')->cascadeOnDelete(); + + $table->string('external_user_id', 255); + $table->string('external_user_result_id', 255); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('lti_user_results'); + Schema::drop('lti_resource_links'); + Schema::drop('lti_contexts'); + Schema::drop('lti_access_tokens'); + Schema::drop('lti_nonces'); + Schema::drop('clients'); + } +}; diff --git a/database/migrations/create_skeleton_table.php.stub b/database/migrations/create_skeleton_table.php.stub deleted file mode 100644 index 2efdce9..0000000 --- a/database/migrations/create_skeleton_table.php.stub +++ /dev/null @@ -1,19 +0,0 @@ -id(); - - // add fields - - $table->timestamps(); - }); - } -}; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a91953b..f082b99 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,14 +1,6 @@ -includes: - - phpstan-baseline.neon - parameters: - level: 4 + level: 6 paths: - src - - config - - database tmpDir: build/phpstan - checkOctaneCompatibility: true - checkModelProperties: true checkMissingIterableValueType: false - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e953c0e..5c3e79c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,7 @@ backupStaticProperties="false" > - + tests diff --git a/src/Commands/SkeletonCommand.php b/src/Commands/SkeletonCommand.php deleted file mode 100644 index 3e5f628..0000000 --- a/src/Commands/SkeletonCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -comment('All done'); - - return self::SUCCESS; - } -} diff --git a/src/Facades/Skeleton.php b/src/Facades/Skeleton.php deleted file mode 100644 index 1fa9076..0000000 --- a/src/Facades/Skeleton.php +++ /dev/null @@ -1,16 +0,0 @@ -name('swis-laravel-lti-provider') + ->hasMigration('2023_10_26_100000_add_client_and_lti_tables') + ->runsMigrations() + ->hasInstallCommand(function (InstallCommand $command) { + $command + ->publishConfigFile() + ->askToStarRepoOnGitHub('swisnl/laravel-lti-provider'); + }); + } +} diff --git a/src/ModelDataConnector.php b/src/ModelDataConnector.php new file mode 100644 index 0000000..f3824f7 --- /dev/null +++ b/src/ModelDataConnector.php @@ -0,0 +1,553 @@ +environment = $environment; + } + + public function loadPlatform(Platform $platform): bool + { + if (! app()->bound(LtiClient::class) || ! app(LtiClient::class) instanceof Model) { + return false; + } + + if (! empty($platform->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\Contracts\LtiClient|null $client */ + $client = app(LtiClient::class)::firstWhere('nr', $platform->getRecordId()); + if (! $client) { + return false; + } + + $client->fillLtiPlatform($platform); + + return true; + } + + if (! empty($platform->platformId) || ! empty($platform->clientId) || ! empty($platform->deploymentId)) { + $query = app(LtiClient::class)::query(); + + if (! empty($platform->platformId)) { + $query->where('lti_platform_id', $platform->platformId); + } + if (! empty($platform->clientId)) { + $query->where('lti_client_id', $platform->clientId); + } + if (! empty($platform->deploymentId)) { + $query->where('lti_deployment_id', $platform->deploymentId); + } + + /** @var \Swis\Laravel\LtiProvider\Models\Contracts\LtiClient|null $client */ + $client = $query->first(); + if (! $client) { + return false; + } + + $client->fillLtiPlatform($platform); + + return true; + } + + if (! empty($platform->getKey())) { + /** @var \Swis\Laravel\LtiProvider\Models\Contracts\LtiClient|null $client */ + $client = app(LtiClient::class)::find($platform->getKey()); + if (! $client) { + return false; + } + + $client->fillLtiPlatform($platform); + + return true; + } + + return false; + } + + public function savePlatform(Platform $platform): bool + { + if (! app()->bound(LtiClient::class)) { + return false; + } + + $LtiClient = app(LtiClient::class); + + if (! $LtiClient instanceof Model) { + return false; + } + + if (! empty($platform->getRecordId())) { + $client = $LtiClient::firstWhere('nr', $platform->getRecordId()); + if (! $client || ! $client instanceof LtiClient) { + return false; + } + + $this->fixPlatformSettings($platform, true); + $client->fillFromLtiPlatform($platform); + $this->fixPlatformSettings($platform, false); + $client->save(); + } else { + return false; + } + + $platform->updated = $client->updated_at->getTimestamp(); + + return true; + } + + public function deletePlatform(Platform $platform): bool + { + return false; + } + + /** + * @return \ceLTIc\LTI\Platform[] + */ + public function getPlatforms(): array + { + if (! app()->bound(LtiClient::class)) { + return []; + } + + $LtiClient = app(LtiClient::class); + + if (! $LtiClient instanceof Model) { + return []; + } + + /** @var Collection $clients. */ + $clients = $LtiClient::all(); + + return $clients->map(function (LtiClient $client) { + $platform = new Platform($this); + $client->fillLtiPlatform($platform); + })->values()->toArray(); + } + + public function loadContext(Context $context): bool + { + if (! empty($context->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiContext|null $ltiContext */ + $ltiContext = $this->environment->contexts()->with('client')->find($context->getRecordId()); + if (! $ltiContext) { + return false; + } + + $ltiContext->fillLtiContext($context); + + return true; + } + + if (! empty($context->ltiContextId)) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiContext|null $ltiContext */ + $ltiContext = $this->environment->contexts()->with('client')->where('external_context_id', $context->ltiContextId) + ->whereHas('client', function ($query) use ($context) { + $query->where('id', $context->getPlatform()->getKey()); + }) + ->first(); + if (! $ltiContext) { + return false; + } + + $ltiContext->fillLtiContext($context); + + return true; + } + + return false; + } + + public function saveContext(Context $context): bool + { + if (! empty($context->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiContext|null $ltiContext */ + $ltiContext = $this->environment->contexts()->find($context->getRecordId()); + if (! $ltiContext) { + return false; + } + + $ltiContext->fillFromLtiContext($context); + $ltiContext->save(); + } else { + /** @var \Swis\Laravel\LtiProvider\Models\LtiContext|null $ltiContext */ + $ltiContext = $this->environment->contexts()->make(); + $ltiContext->fillFromLtiContext($context); + $ltiContext->save(); + + $context->setRecordId($ltiContext->id); + $context->created = $ltiContext->created_at->getTimestamp(); + } + + $context->updated = $ltiContext->updated_at->getTimestamp(); + + return true; + } + + public function deleteContext(Context $context): bool + { + if (! empty($context->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiContext|null $ltiContext */ + $ltiContext = $this->environment->contexts()->find($context->getRecordId()); + if (! $ltiContext) { + return false; + } + + $ltiContext->delete(); + + return true; + } + + return false; + } + + public function loadResourceLink(ResourceLink $resourceLink): bool + { + if (! empty($resourceLink->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->with('client')->find($resourceLink->getRecordId()); + if (! $ltiResourceLink) { + return false; + } + + $ltiResourceLink->fillLtiResourceLink($resourceLink); + + return true; + } + + if (! empty($resourceLink->getContext())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->with('client')->where('external_resource_link_id', $resourceLink->ltiResourceLinkId) + ->where(function ($query) use ($resourceLink) { + $query + ->where('lti_context_id', $resourceLink->getContext()->getRecordId()) + ->orWhereIn('client_id', function ($query) use ($resourceLink) { + $query + ->select('client_id') + ->from('lti_contexts') + ->where('id', $resourceLink->getContext()->getRecordId()); + }); + }) + ->first(); + + if (! $ltiResourceLink) { + return false; + } + + $ltiResourceLink->fillLtiResourceLink($resourceLink); + + return true; + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->with('client')->where('external_resource_link_id', $resourceLink->ltiResourceLinkId) + ->where(function ($query) use ($resourceLink) { + $query + ->where('client_id', $resourceLink->getPlatform()->getKey()) + ->orWhereHas('context', function ($query) use ($resourceLink) { + $query->where('client_id', $resourceLink->getPlatform()->getKey()); + }); + }) + ->first(); + if (! $ltiResourceLink) { + return false; + } + + $ltiResourceLink->fillLtiResourceLink($resourceLink); + + return true; + } + + public function saveResourceLink(ResourceLink $resourceLink): bool + { + if (! empty($resourceLink->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->find($resourceLink->getRecordId()); + if (! $ltiResourceLink) { + return false; + } + + $ltiResourceLink->fillFromLtiResourceLink($resourceLink); + $ltiResourceLink->save(); + } else { + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->make(); + $ltiResourceLink->fillFromLtiResourceLink($resourceLink); + $ltiResourceLink->save(); + + $resourceLink->setRecordId($ltiResourceLink->id); + $resourceLink->created = $ltiResourceLink->created_at->getTimestamp(); + } + + $resourceLink->updated = $ltiResourceLink->updated_at->getTimestamp(); + + return true; + } + + public function deleteResourceLink(ResourceLink $resourceLink): bool + { + if (! empty($resourceLink->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiResourceLink|null $ltiResourceLink */ + $ltiResourceLink = $this->environment->resourceLinks()->find($resourceLink->getRecordId()); + if (! $ltiResourceLink) { + return false; + } + + $ltiResourceLink->delete(); + $resourceLink->initialize(); + + return true; + } + + return false; + } + + /** + * @return \ceLTIc\LTI\UserResult[] + */ + public function getUserResultSourcedIDsResourceLink(ResourceLink $resourceLink, bool $localOnly, ?IdScope $idScope): array + { + $userResults = $this->environment->userResults()->where('lti_resource_link_id', $resourceLink->getRecordId()) + ->get()->values()->map(function (LtiUserResult $ltiUserResult) use ($resourceLink) { + $userResult = new UserResult(); + + $userResult->setDataConnector($this); + $userResult->setResourceLinkId($resourceLink->getRecordId()); + $userResult->setResourceLink($resourceLink); + + $ltiUserResult->fillLtiUserResult($userResult); + + return $userResult; + }); + + if (! is_null($idScope)) { + return $userResults->mapWithKeys(function (UserResult $userResult) use ($idScope) { + return [$userResult->getId($idScope) => $userResult]; + })->toArray(); + } + + return $userResults->toArray(); + } + + public function getSharesResourceLink(ResourceLink $resourceLink): array + { + return []; + } + + public function loadPlatformNonce(PlatformNonce $nonce): bool + { + if (parent::useMemcache()) { + return parent::loadPlatformNonce($nonce); + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiNonce|null $ltiNonce */ + $ltiNonce = $this->environment->nonces()->where('client_id', $nonce->getPlatform()->getKey()) + ->where('nonce', $nonce->getValue()) + ->where('expires_at', '>', Carbon::now()) + ->first(); + + if (! $ltiNonce) { + return false; + } + + $ltiNonce->fillLtiPlatformNonce($nonce); + + return true; + } + + public function savePlatformNonce(PlatformNonce $nonce): bool + { + if (parent::useMemcache()) { + return parent::savePlatformNonce($nonce); + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiNonce $ltiNonce */ + $ltiNonce = $this->environment->nonces()->firstOrNew([ + 'client_id' => $nonce->getPlatform()->getKey(), + 'nonce' => $nonce->getValue(), + ]); + + $ltiNonce->fillFromLtiPlatformNonce($nonce); + $ltiNonce->save(); + + return true; + } + + public function deletePlatformNonce(PlatformNonce $nonce): bool + { + if (parent::useMemcache()) { + return parent::deletePlatformNonce($nonce); + } + + $this->environment->nonces()->where('client_id', $nonce->getPlatform()->getKey()) + ->where('nonce', $nonce->getValue()) + ->delete(); + + return true; + } + + public function loadAccessToken(AccessToken $accessToken): bool + { + if (parent::useMemcache()) { + return parent::loadAccessToken($accessToken); + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiAccessToken|null $ltiAccessToken */ + $ltiAccessToken = $this->environment->accessTokens()->where('client_id', $accessToken->getPlatform()->getKey()) + ->first(); + + if (! $ltiAccessToken) { + return false; + } + + $ltiAccessToken->fillLtiAccessToken($accessToken); + + return true; + } + + public function saveAccessToken(AccessToken $accessToken): bool + { + if (parent::useMemcache()) { + return parent::saveAccessToken($accessToken); + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiAccessToken $ltiAccessToken */ + $ltiAccessToken = $this->environment->accessTokens()->firstOrNew([ + 'client_id' => $accessToken->getPlatform()->getKey(), + ]); + $ltiAccessToken->fillFromLtiAccessToken($accessToken); + $ltiAccessToken->save(); + + return true; + } + + public function loadResourceLinkShareKey(ResourceLinkShareKey $shareKey): bool + { + return false; + } + + public function saveResourceLinkShareKey(ResourceLinkShareKey $shareKey): bool + { + return false; + } + + public function deleteResourceLinkShareKey(ResourceLinkShareKey $shareKey): bool + { + return false; + } + + public function loadUserResult(UserResult $userResult): bool + { + if (! empty($userResult->getRecordId())) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiUserResult|null $ltiUserResult */ + $ltiUserResult = $this->environment->userResults()->find($userResult->getRecordId()); + if (! $ltiUserResult) { + return false; + } + + $ltiUserResult->fillLtiUserResult($userResult); + + return true; + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiUserResult|null $ltiUserResult */ + $ltiUserResult = $this->environment->userResults()->where('lti_resource_link_id', $userResult->getResourceLink()->getRecordId()) + ->where('external_user_id', $userResult->getId(IdScope::IdOnly)) + ->first(); + if (! $ltiUserResult) { + return false; + } + + $ltiUserResult->fillLtiUserResult($userResult); + + return true; + } + + public function saveUserResult(UserResult $userResult): bool + { + if (is_null($userResult->created)) { + /** @var \Swis\Laravel\LtiProvider\Models\LtiUserResult $ltiUserResult */ + $ltiUserResult = $this->environment->userResults()->make(); + $ltiUserResult->fillFromLtiUserResult($userResult); + $ltiUserResult->save(); + + $userResult->setRecordId($ltiUserResult->id); + $userResult->created = $ltiUserResult->created_at->getTimestamp(); + $userResult->updated = $ltiUserResult->updated_at->getTimestamp(); + + return true; + } + + /** @var \Swis\Laravel\LtiProvider\Models\LtiUserResult|null $ltiUserResult */ + $ltiUserResult = $this->environment->userResults()->find($userResult->getRecordId()); + if (! $ltiUserResult) { + return false; + } + + $ltiUserResult->fillFromLtiUserResult($userResult); + $ltiUserResult->save(); + + $userResult->updated = $ltiUserResult->updated_at->getTimestamp(); + + return true; + } + + public function deleteUserResult(UserResult $userResult): bool + { + /** @var \Swis\Laravel\LtiProvider\Models\LtiUserResult|null $ltiUserResult */ + $ltiUserResult = $this->environment->userResults()->find($userResult->getRecordId()); + if (! $ltiUserResult) { + return false; + } + + $ltiUserResult->delete(); + + return true; + } + + public function loadTool(Tool $tool): bool + { + throw new \Exception('loadTool not implemented'); + } + + public function saveTool(Tool $tool): bool + { + throw new \Exception('saveTool not implemented'); + } + + public function deleteTool(Tool $tool): bool + { + throw new \Exception('deleteTool not implemented'); + } + + /** + * @return \ceLTIc\LTI\Tool[] + */ + public function getTools(): array + { + throw new \Exception('getTools not implemented'); + } +} diff --git a/src/Models/Contracts/LtiClient.php b/src/Models/Contracts/LtiClient.php new file mode 100644 index 0000000..243f9ef --- /dev/null +++ b/src/Models/Contracts/LtiClient.php @@ -0,0 +1,40 @@ + + */ + public function resourceLinks(): HasMany; + + /** + * @return HasMany<\Swis\Laravel\LtiProvider\Models\LtiContext> + */ + public function contexts(): HasMany; + + /** + * @return HasMany<\Swis\Laravel\LtiProvider\Models\LtiNonce> + */ + public function nonces(): HasMany; + + /** + * @return HasMany<\Swis\Laravel\LtiProvider\Models\LtiAccessToken> + */ + public function accessTokens(): HasMany; +} diff --git a/src/Models/Contracts/LtiEnvironment.php b/src/Models/Contracts/LtiEnvironment.php new file mode 100644 index 0000000..2169297 --- /dev/null +++ b/src/Models/Contracts/LtiEnvironment.php @@ -0,0 +1,33 @@ + + */ + public function accessTokens(): \Illuminate\Database\Eloquent\Relations\MorphMany; + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiContext> + */ + public function contexts(): \Illuminate\Database\Eloquent\Relations\MorphMany; + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiNonce> + */ + public function nonces(): \Illuminate\Database\Eloquent\Relations\MorphMany; + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiResourceLink> + */ + public function resourceLinks(): \Illuminate\Database\Eloquent\Relations\MorphMany; + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiUserResult> + */ + public function userResults(): \Illuminate\Database\Eloquent\Relations\MorphMany; +} diff --git a/src/Models/LtiAccessToken.php b/src/Models/LtiAccessToken.php new file mode 100644 index 0000000..3f079b4 --- /dev/null +++ b/src/Models/LtiAccessToken.php @@ -0,0 +1,92 @@ + 'array', + 'expires_at' => 'datetime', + ]; + + /** + * @var array + */ + protected $attributes = [ + 'scopes' => '[]', + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, self> + */ + public function client(): BelongsTo + { + return $this->belongsTo(app(LtiClient::class)); + } + + public function fillLtiAccessToken(AccessToken $accessToken): void + { + $accessToken->scopes = $this->scopes; + $accessToken->token = $this->access_token; + $accessToken->expires = $this->expires_at->getTimestamp(); + $accessToken->created = $this->created_at->getTimestamp(); + $accessToken->updated = $this->updated_at->getTimestamp(); + } + + public function fillFromLtiAccessToken(AccessToken $accessToken): void + { + $this->scopes = $accessToken->scopes; + $this->access_token = $accessToken->token; + $this->expires_at = Carbon::createFromTimestamp($accessToken->expires); + } +} diff --git a/src/Models/LtiContext.php b/src/Models/LtiContext.php new file mode 100644 index 0000000..09f78d1 --- /dev/null +++ b/src/Models/LtiContext.php @@ -0,0 +1,105 @@ + $resourceLinks + * @property int|null $resource_links_count + * + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext query() + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereClientId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereExternalContextId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereLtiEnvironmentId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereLtiEnvironmentType($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereSettings($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereTitle($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiContext whereUpdatedAt($value) + * + * @mixin \Eloquent + */ +class LtiContext extends Model +{ + use HasLtiEnvironment; + + protected $fillable = [ + 'client_id', + 'external_context_id', + 'title', + 'settings', + ]; + + protected $casts = [ + 'settings' => AsArrayObject::class, + ]; + + /** + * @var array + */ + protected $attributes = [ + 'settings' => '{}', + ]; + + public function fillLtiContext(Context $context): void + { + $context->setRecordId($this->id); + + $context->setPlatformId($this->client->nr); + + $context->title = $this->title; + $context->ltiContextId = $this->external_context_id; + $context->setSettings($this->settings->toArray()); + } + + public function fillFromLtiContext(Context $context): void + { + $this->client_id = $context->getPlatform()->getKey(); + + $this->title = $context->title; + $this->external_context_id = $context->ltiContextId; + $this->settings = new ArrayObject($context->getSettings()); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, self> + */ + public function client(): BelongsTo + { + return $this->belongsTo(app(LtiClient::class)); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Swis\Laravel\LtiProvider\Models\LtiResourceLink> + */ + public function resourceLinks(): HasMany + { + return $this->hasMany(LtiResourceLink::class, 'lti_context_id'); + } +} diff --git a/src/Models/LtiNonce.php b/src/Models/LtiNonce.php new file mode 100644 index 0000000..1a3ea1e --- /dev/null +++ b/src/Models/LtiNonce.php @@ -0,0 +1,79 @@ + 'datetime', + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, self> + */ + public function client(): BelongsTo + { + return $this->belongsTo(app(\Swis\Laravel\LtiProvider\Models\Contracts\LtiClient::class)); + } + + public static function deleteExpired(): void + { + self::where('expires_at', '<', now())->delete(); + } + + public function fillLtiPlatformNonce(PlatformNonce $nonce): void + { + $nonce->expires = $this->expires_at->getTimestamp(); + } + + public function fillFromLtiPlatformNonce(PlatformNonce $nonce): void + { + $this->expires_at = Carbon::createFromTimestamp($nonce->expires); + } +} diff --git a/src/Models/LtiResourceLink.php b/src/Models/LtiResourceLink.php new file mode 100644 index 0000000..b5d7a37 --- /dev/null +++ b/src/Models/LtiResourceLink.php @@ -0,0 +1,130 @@ + $userResults + * @property int|null $user_results_count + * + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink query() + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereClientId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereExternalResourceLinkId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereLtiContextId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereLtiEnvironmentId($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereLtiEnvironmentType($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereSettings($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereTitle($value) + * @method static \Illuminate\Database\Eloquent\Builder|LtiResourceLink whereUpdatedAt($value) + * + * @mixin \Eloquent + */ +class LtiResourceLink extends Model +{ + use HasLtiEnvironment; + + protected $fillable = [ + 'client_id', + 'lti_context_id', + 'title', + 'external_resource_link_id', + 'settings', + ]; + + protected $casts = [ + 'settings' => AsArrayObject::class, + ]; + + /** + * @var array + */ + protected $attributes = [ + 'settings' => '{}', + ]; + + protected static function boot() + { + parent::boot(); + + static::saving(function (LtiResourceLink $model) { + if (empty($model->client_id) && ! empty($model->lti_context_id)) { + $model->client_id = LtiContext::find($model->lti_context_id)?->client_id; + } + }); + } + + public function fillLtiResourceLink(ResourceLink $resourceLink): void + { + $resourceLink->setRecordId($this->id); + + $resourceLink->setPlatformId($this->client->nr); + $resourceLink->setContextId($this->lti_context_id); + + $resourceLink->title = $this->title; + $resourceLink->ltiResourceLinkId = $this->external_resource_link_id; + $resourceLink->setSettings($this->settings->toArray()); + } + + public function fillFromLtiResourceLink(ResourceLink $resourceLink): void + { + $this->client_id = $resourceLink->getPlatform()->getKey(); + $this->lti_context_id = $resourceLink->getContext()?->getRecordId(); + + $this->title = $resourceLink->title; + $this->external_resource_link_id = $resourceLink->ltiResourceLinkId; + $this->settings = new ArrayObject($resourceLink->getSettings()); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, self> + */ + public function client(): BelongsTo + { + return $this->belongsTo(app(LtiClient::class)); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Swis\Laravel\LtiProvider\Models\LtiContext, self> + */ + public function context(): BelongsTo + { + return $this->belongsTo(LtiContext::class, 'lti_context_id'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany<\Swis\Laravel\LtiProvider\Models\LtiUserResult> + */ + public function userResults(): HasMany + { + return $this->hasMany(LtiUserResult::class, 'lti_resource_link_id'); + } +} diff --git a/src/Models/LtiUserResult.php b/src/Models/LtiUserResult.php new file mode 100644 index 0000000..e5ac11b --- /dev/null +++ b/src/Models/LtiUserResult.php @@ -0,0 +1,75 @@ +setRecordId($this->id); + + $userResult->setResourceLinkId($this->lti_resource_link_id); + + $userResult->ltiUserId = $this->external_user_id; + $userResult->ltiResultSourcedId = $this->external_user_result_id; + } + + public function fillFromLtiUserResult(UserResult $userResult): void + { + $this->lti_resource_link_id = $userResult->getResourceLink()->getRecordId(); + $this->external_user_id = $userResult->getId(IdScope::IdOnly); + $this->external_user_result_id = $userResult->ltiResultSourcedId; + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Swis\Laravel\LtiProvider\Models\LtiResourceLink, self> + */ + public function resourceLink(): BelongsTo + { + return $this->belongsTo(\Swis\Laravel\LtiProvider\Models\LtiResourceLink::class, 'lti_resource_link_id'); + } +} diff --git a/src/Models/Traits/HasLtiEnvironment.php b/src/Models/Traits/HasLtiEnvironment.php new file mode 100644 index 0000000..9a3a7b8 --- /dev/null +++ b/src/Models/Traits/HasLtiEnvironment.php @@ -0,0 +1,18 @@ + + */ + public function ltiEnvironment(): MorphTo + { + return $this->morphTo('lti_environment'); + } +} diff --git a/src/Models/Traits/IsLtiEnvironment.php b/src/Models/Traits/IsLtiEnvironment.php new file mode 100644 index 0000000..3f7ffb8 --- /dev/null +++ b/src/Models/Traits/IsLtiEnvironment.php @@ -0,0 +1,55 @@ + + */ + public function accessTokens(): \Illuminate\Database\Eloquent\Relations\MorphMany + { + return $this->morphMany(\Swis\Laravel\LtiProvider\Models\LtiAccessToken::class, 'lti_environment'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiContext> + */ + public function contexts(): \Illuminate\Database\Eloquent\Relations\MorphMany + { + return $this->morphMany(\Swis\Laravel\LtiProvider\Models\LtiContext::class, 'lti_environment'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiNonce> + */ + public function nonces(): \Illuminate\Database\Eloquent\Relations\MorphMany + { + return $this->morphMany(\Swis\Laravel\LtiProvider\Models\LtiNonce::class, 'lti_environment'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiResourceLink> + */ + public function resourceLinks(): \Illuminate\Database\Eloquent\Relations\MorphMany + { + return $this->morphMany(\Swis\Laravel\LtiProvider\Models\LtiResourceLink::class, 'lti_environment'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\Swis\Laravel\LtiProvider\Models\LtiUserResult> + */ + public function userResults(): \Illuminate\Database\Eloquent\Relations\MorphMany + { + return $this->morphMany(\Swis\Laravel\LtiProvider\Models\LtiUserResult::class, 'lti_environment'); + } + + public function getDataConnector(): ModelDataConnector + { + return new \Swis\Laravel\LtiProvider\ModelDataConnector($this); + } +} diff --git a/src/Skeleton.php b/src/Skeleton.php deleted file mode 100755 index 66fab60..0000000 --- a/src/Skeleton.php +++ /dev/null @@ -1,7 +0,0 @@ -name('skeleton') - ->hasConfigFile() - ->hasViews() - ->hasMigration('create_skeleton_table') - ->hasCommand(SkeletonCommand::class); - } -} diff --git a/tests/Pest.php b/tests/Pest.php index 7fe1500..5e5cf74 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,5 @@ in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php index d04fb0c..bd1ede9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,10 +1,10 @@ 'VendorName\\Skeleton\\Database\\Factories\\'.class_basename($modelName).'Factory' + fn (string $modelName) => 'Swis\\LaravelLtiProvider\\Database\\Factories\\'.class_basename($modelName).'Factory' ); } protected function getPackageProviders($app) { return [ - SkeletonServiceProvider::class, + LaravelLtiProviderServiceProvider::class, ]; } @@ -29,7 +29,7 @@ public function getEnvironmentSetUp($app) config()->set('database.default', 'testing'); /* - $migration = include __DIR__.'/../database/migrations/create_skeleton_table.php.stub'; + $migration = include __DIR__.'/../database/migrations/create_laravel-lti-provider_table.php.stub'; $migration->up(); */ }