Developing My First Open Source Package Laravel Rekognition
Oct 1st, 2022 By Taylor Perkins Full Stack Developer
I wanted to preface this article with the fact that I did just release my first experimental version the other day and there do seem to be some issues already. I will dive into the details later in this article, but I just wanted to get that disclaimer out there. The issues do seem to be only Vapor related though. That said, I wanted to dig into my experience of building my first composer package. It was rather fun and I learned so much!
Spatie Laravel Package Tools And Package Skeleton
I think it best to start off with, once again, some of the open-source packages that helped me develop my own package. Huge shoutout to Spatie, if it weren't for them this adventure likely would have taken much longer. Laravel does offer extensive documentation on package development, but the packages from Spatie even offer github actions and all kinds of fun stuff that expedite the learning process. So on to the honorable mentions. First and foremost is Spatie's package skeleton for Laravel. This will be the core skeleton for you to be able to create yourself a Laravel package. The next package, of which this skeleton comes with, is the Laravel package tools. This package gives you all the abstractions and tools you will need to be able to seamlessly create a Laravel package. To top it off, if you're really wanting to dive into learning about creating Laravel packages, you can take their Laravel package training course. Everything you need is abstracted out into this one method, configurePackage. Under the hood the PackageServiceProvider does all the stuff in the boot and register methods based on what you define with your Spatie\LaravelPackageTools\Package object. Basic usage is even clearly defined in their repos' README. You can define publishable assets, migrations, a config file of course. Everything has been thought through and provided for you.
Let's take a closer look at how we might be able to expedite creating your own Laravel package. The skeleton provides you with an initial configure command which replaces a lot of the placeholder values in this package. You can see some of them in the README of the skeleton package. It'll ask for your name, github username, vendor name and namespace etc. Then it will ask you about enabling other packages that will help with developing a clean Laravel package. These include PhpStan (for static code analysis), Laravel pint (for abiding to opinionated coding style guidelines using phpcs), and Ray for debugging. There is also some workflows that you can enable such as Dependabot and and Auto-Changelog. You can read more about Dependabot and Auto-Changelog, I installed and enabled everything offered, as I assumed it was all intentional and came in knowing nothing about package development. I honestly don't know too much about the last two workflows there. What I have observed so far is they are both there to make sure your package create and maintenance workflow is automated as much as possible. I believe auto-changelog does something to the nature of taking commit messages from PR to document version changes and dependabot keeps your package dependencies up to date to avoid security issues. I am an avid learn by doing advocate, and therefore recommend and will update y'all when I learn more about these workflows. Now that we have the basic usage and packages introduced, let's take a look at the package I built.
Developing An Open Source Laravel Facade
DISCLAIMER: I have already one open issue. I noticed after I installed into this blog application, that the config file doesn't want to be published for some reason. While everything works locally just fine, I believe this could possibly be causing issues when used with Vapor, but am unsure. I will update everyone when I learn more and fix the bug.
My Laravel Rekognition package is my first ever! I am super excited to learn more about contributing to the community and maintaining open-source packages. I am just learning and picking things up as I go, and am always open to feedback and constructive criticism. That said, let me walk you through some of the things I learned along the way and what it is that I built.
In my endeavors to learn, I am always looking for cool exciting features to implement. This was one of those. While not all that useful for the purposes of this blog app, I wanted to figure it out anyways. Since I am not writing that many blog articles, it isn't that much effort at all to manually type in a featured image alt tag. However, I wanted to learn how I could use AWS Rekognition to automatically detect labels and assign an alt tag to my featured images. As you can probably deduce, this is total overkill for my little blog application. I had lots of fun implementing it though, and then took it to the next step to share my work with the world by creating a package out of it. All the package does for you is gives you a Laravel facade that gives you one method: getFromFilePath.
You simply pass a file path to the facade. I always use the Storage facade for anything filesystem related, so here is a basic usage example. Remember, this is just an example, and your implementation of whatever media you pass to this is up to you. So this example may not make complete sense in your context.
1use LaravelGeek\LaravelRekognition\Facades\Rekognition;2 3$imageLabels = Rekognition::getFromFilePath(Storage::path($media));4// do something with your labels string
Under the hood there's really not that much going on. When the facade is instantiated, the constructor calls a method to instantiate our RekognitionClient. This is provided to us with the open-source aws/aws-sdk-php package, which is a dependency in this package. The Credentials is another class given to us by AWS. Here is what that looks like:
1use Aws\Credentials\Credentials; 2use Aws\Rekognition\RekognitionClient; 3 4if (is_null($this->client)) { 5 $this->client = new RekognitionClient([ 6 'credentials' => new Credentials(config('rekognition.key'), config('rekognition.secret')), 7 'region' => config('rekognition.region'), 8 'version' => 'latest', 9 ]);10}
Then finally it is as simple as calling the detectLabels method on the RekognitionClient. This returns an \Aws\Result object back, which has a bunch of information other than just the image labels themselves. Stuff like confidence levels and such. So in the case of my Facade, I just grabbed the label names from this result by passing in a minConfidence, max number of labels and the file contents and returning an imploded string of those labels. Here's a snippet of that logic. Of course, you can go checkout the repo yourself for the full context.
1$result = $this->client->detectLabels([2 'Image' => [3 'Bytes' => $fileContents,4 ],5 'MaxLabels' => 12,6 'MinConfidence' => 65.00,7]);8 9return collect($result->get('Labels'))->implode('Name', ', ');
Now that we have examined the core logic of the magic behind the Facade, I wanted to dig into the exciting details of turning this into a composer package.
Creating A Composer Package For Laravel
When first setting up the package skeleton, You are going to see a lot of directories and migration stubs and such. I am going to be using the LaravelRekognition package as my example while walking through the steps. In this example, I have removed most of this stuff since my package didn't require migrations or views. All I really needed was a config file, a facade, the magic behind the facade and a ServiceProvider (of which is provided to you by the package skeleton). Now let's assume you're requirements are similar and let's go ahead and remove the resources/views directory, as well the database directory. When running the configure script, all the Class names will be replaced. Since my package was called LaravelRekognition, I have a LaravelRekognitionServiceProvider. You will want to define your assets and such in here. See spatie's README for more info on all your options, or take their course to learn more. My requirements were fairly simple, so here is what my configurePackage method looks like.
1public function configurePackage(Package $package): void 2{ 3 /* 4 * This class is a Package Service Provider 5 * 6 * More info: https://github.com/spatie/laravel-package-tools 7 */ 8 $package 9 ->name('laravel-rekognition')10 ->hasConfigFile();11 12 $this->app->bind(LaravelRekognition::class, function () {13 return new LaravelRekognition();14 });15}
All I am doing here is defining the name of my package, and defining a publishable config file. There is a naming convention there, which I believe might be why my config file is currently not publishable. Then we're just going to bind our facade (the skeleton comes with one and renames it to your package name) to the Laravel app instance. As far as the facade itself goes, you shouldn't have to change much, if anything. The skeleton came with a facade and a class for the facade. I did wind up changing the facade class name, and added my method annotation to the doc block, but that was it. The real magic you've already seen above is the getFromFilePath method in the LaravelRekognition class that the facade references.
Pro tip: one last note I need to mention. I want to be loose with this guide, you're requirements are likely different. That said, you'll need to update your composer.json if you plan on getting rid of your migration stubs and such. Autoload will fail once you've removed those directories.
The last piece is our config file. My requirements were an AWS access key, secret and region. If you use Laravel Vapor, or an s3 bucket as your filesystem, then you likely already have these readily available. That pretty much concludes all the code I had to write in order to create this package. I do have some tests and such that we'll take a look at. Here is what the my config looks like and then we'll move on to launching your new Laravel package.
1return [2 'key' => env('AWS_ACCESS_KEY_ID'),3 'secret' => env('AWS_SECRET_ACCESS_KEY'),4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),5];
Signing Up For Packagist And Automating Releases
Now we're on to the last few steps. This part is rather quick. Since you may be like me and never created a composer package before, you will have to sign up for an account at packagist.org. They actually provide steps on how to get your package published as well. It's pretty much as simple as having a public github or other repository and that's it. If you followed the steps in the skeleton package, after you've created your account, you should be able to simply submit your package with the associated public github repository and you will soon have a composer installable package. Packagist will crawl your public repo and gather all the required information for you. I think packagist will even crawl your repo periodically without you having to submit any changes, but let's take a look at automating this workflow.
After you've created your package, you should see some information about it in the packagist url for it. It will show dependencies, release versions and even the README. If my memory serves me correct, there will also be some info/warning dialog on the page about how your repo is not auto-updated or so. I think all I did was click the info/warning dialog about not being auto-updated, and oauth with Github from there. Packagist also has the Github hook documented on their about page if you need more thorough step by step. Now, on to the last step!
Deploying And Testing Your New Laravel Package
While this last step technically is not required, I thoroughly enjoyed having the github actions and tests. My package was very minimal, but even my two simple pest tests definitely helped me with making this package usable for everyone. I had misconfigured my aws/aws-sdk-php dependency version in my composer.json. I had done an initial local install with aws at the max latest version in my compser.json to test out the new pacakge. I went to composer require and had dependency conflicts (this blog project uses the same aws/aws-sdk-php dependency) because my required versions were not compatible. I reacted to this by just changing to a much lower version than it should have been.
1"require": {2 "php": "^8.1",3 "aws/aws-sdk-php": "^3.10",4 "illuminate/contracts": "^9.0",5 "spatie/laravel-package-tools": "^1.0.0"6},
Everything seemingly worked fine, until I started to cleanup the repo and actually try to get all the unit tests passing. Enter, the run-tests Github action already defined by the skeleton package. This was such a great helper and I am so grateful for the skeleton package. It turns out, that some of my tests were failing due to the prefer-lowest tests. There's two jobs or tests defined, which both run your unit tests, but they "prefer" the lowest package dependency version, or the stable version, defined by your composer.json. Or at least that's how I interpreted whats going. This proved a fatal error in my knee jerk reaction to a much lower aws version. It turns out, the RekognitionClient was not implemented until 3.26, which I had lowered it to ^3.10 in my composer.json. The test was telling me I failed to take everything into account. I had to go and read through the changelog and releases until I could find the exact version where the RekognitionClient was introduced. Now let's take a look at the final step to deploying your versioned package.
Last, but not least, is to tag a release version. In my case, I wanted to release an initial experimental version. All I did was run a couple commands, and with the Github hook enabled and automated Github actions, it was as simple as this.
1git tag 0.0.12git push --tags
Conclusion
I am relatively new to unit testing in general, but it just goes to show you how important they really are. None of my tests directly tested version requirements or anything, but simply having even a single test would have caught this issue. Although I am new to unit testing, I am already a huge proponent of unit tests.
I hope that I have helped at least one person in creating their own package by sharing my adventures and mishaps. I simply want to share and document knowledge and mistakes I have made with the community. That said, I am always open for questions or comments, If I have left a step out that has left you confused or lost, please don't hesitate to reach out via twitter, or email.
Coming soon, I will be implementing comments as a first step towards having a commonplace to share knowledge and building things together. I hope this will encourage more discussions and questions in regards to the fun stuff I talk about here.
Final reminder, I will be looking into the bugs in my LaravelRekognition package today, and I hope to be able to update y'all with a solution soon.
Please sign in or create an account to join the conversation.