Borderlist – I made a thing

So listen, I made a thing. Yes, it is another to do app thingie but this one is built a bit differently. I work from so many lists on any given day that it is difficult to see everything at a glance.

I want to

  • See multiple lists at a time around the same topic.
  • Make a list, add some items, get stuff done, tick’em off.
  • Delete lists and clear my boards.
  • See it all on my phone without having to download yet another app.

No bells and whistles. No fluff.
I want a disposable Post-It Note system without all the sticky pads.

With these goals in mind I built Borderlist. It’s free, simple, and I use it every day.

Visit Borderlist

Because this is a tech blog, here are some nerdy details. The front end is built with Bulma (CSS) and, once in an account, it is all Vue using a Laravel based API to handle the data.

If you decide to give it a try, let me know what you think.

Setting Up, Creating and Updating a hasOne Relationship in Laravel

Not a lot on creating and updating a hasOne record so here’s what ended up working for me. I have my Site model and a SiteCache. SiteCache should have a single record per Site.

SiteCache migration (the Site migration isn’t that important so I omitted it here)

<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateSiteCachesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('site_caches', function (Blueprint $table) { $table->longText('json_content'); $table->unsignedInteger('site_id'); $table->foreign('site_id')->references('id')->on('sites')->onDelete('cascade'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('site_caches'); } }

Site Model

<?php namespace App; class Site extends Base // base is a model that I extend, nothing of note in it { public function cache() { return $this->hasOne(SiteCache::class); } public function setCacheContent($content) { $data = ['json_content' => $content]; if($cache = $this->cache()->first()) { $cache->update($data); } else { $this->cache()->save( new SiteCache($data) ); } return $this; } }

SiteCache Model

<?php namespace App; class SiteCache extends Base { public function site() { return $this->belongsTo(Site::class); } }

In use

<?php $site = Site::first(); $site->setCacheContent($json);

The creating and updating of the cache record on the SiteCache page isn’t as fluent as I’d like but with a little helper method on the Site model all is good.

Using Foreign Keys with SQLite in Laravel

When using SQLite for in-memory tests, I discovered that foreign keys are not used by default. Not so bad until you test cascading deletes and cry rivers of ones and zeros… Do I really have to make a test database?! I thought this was America! WHHHYYYYYYYYYYY?!?!?!?!?!?!

With a bunch of Googling I found that it is possible to enable foreign keys in SQLite. They are just turned off by default to cause sadness and confusion. To enable, you can use the code below.

if (DB::connection() instanceof Illuminate\Database\SQLiteConnection) { DB::statement(DB::raw('PRAGMA foreign_keys=1')); }

But where to put it? I don’t want that thing chilling in my app. I only need it for testing at the moment. I extend the TestCase class for all my tests so I dropped it in the createApplication method like so:

abstract class TestCase extends Illuminate\Foundation\Testing\TestCase { protected $baseUrl = 'http://localhost'; public function createApplication() { $app = require __DIR__.'/../bootstrap/app.php'; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); // when using sqlite, allow foreign keys if (DB::connection() instanceof Illuminate\Database\SQLiteConnection) { DB::statement(DB::raw('PRAGMA foreign_keys=1')); } return $app; } }

I hope that helps someone. Here’s one of the articles that I pulled code from.

Unit Testing with a real Laravel UploadedFile object

I’ve tried, off and on, to unit test file upload objects in various Laravel projects with no real success. I don’t want to mock it and I don’t want to acceptance test a form. It doesn’t exist yet. I want a real file to so I can make calls over it to make sure I get all the data out I need. That I can detect an image, manipulate it using various packages, see the results and tweak till I’m happy and know that it is working when it goes into the app. In short, I’m TDD’ing the API of an object. If that makes sense.

In any case, after much Googling, testing, documentation look up and reading code I figured it out. Hopefully it helps someone else too.

Here’s the code and a bit of explaination…

/* Get byte size of file by doing the following at bash du -b FILENAMEHERE.JPG */ $file = \Illuminate\Http\UploadedFile::createFromBase( (new Symfony\Component\HttpFoundation\File\UploadedFile( __DIR__ . '/files/img_test_file.jpg', 'img_test_file.jpg', 'image/jpeg', 1993588, null, true )) );

When a file is uploaded to Laravel, a Symfony component creates a UploadedFile object form the $_FILE global. This object changes hands several times and ends up being converted to Laravel’s UploadedFile object. Then it is on to being dropped into the Request object that you can pick up in controllers and such.

What this code does is manually create the Symfony object and then manually converts that over to Laravel’s UploadedFile object. Fun. The arguments you see in the Symfony object instantiation are…

  1. path to the file on disk. I stored it in a folder named “files” in the same directory as the test.
  2. file name.
  3. mime type.
  4. size of the file in bytes.
  5. how I feel inside. Actually it is number of errors. Same thing.
  6. boolean confirming that this is being used for testing.

I hope this makes someones day. I almost laid a golden egg when this emerged from my night of profanity.

Happy coding.

So maybe this TDD stuff is going places…

cause I sure has hell just made a kickass logging trait. Alone I don’t think I could have wrote this straight up as quickly as I did with TDD. Plus, now it is testable for the life of the project. Why the hell haven’t I been doing this all along?!

Here’s a bit of code fo yo ass. Here’s the migration for the logs table:

<?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateLogsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('logs', function (Blueprint $table) { $table->uuid('id'); $table->string('name'); $table->uuid('loggable_id')->nullable(); $table->string('loggable_type')->nullable(); $table->text('body')->nullable(); $table->timestamps(); $table->primary('id'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('logs'); } }

And here the model. Nothing special.

<?php namespace App; class Log extends Base { protected $table = 'logs'; protected $fillable = ['name', 'loggable_id', 'loggable_type', 'body']; public function loggable() { return $this->morphTo(); } }

Here is the trait that evolved thanks to TDD

<?php namespace App\Flux\Components; use App\Log; use Carbon\Carbon; trait LoggableTrait { /** * Creating the model relationships to Logs * * @return mixed */ public function logs() { return $this->morphMany(Log::class, 'loggable'); } /** * Log the passed in message by appending to the current log or create a new log entry. * * @param $message * @return $this */ public function log($message) { $name = $this->currentLogName(); $log = $this->logs()->firstOrNew(['name' => $name]); $log->body .= $this->messagePrefix($log->body) . $message; $log->save(); return $this; } /** * Return the current log * * @return mixed */ public function currentLog() { $name = $this->currentLogName(); return $this->logs()->where('name', $name)->first(); } public function clearLogs() { return $this->logs()->delete(); } /** * Return the current log's name build from the current date and model class name. * * @return string */ protected function currentLogName() { return Carbon::now()->format('Y-m-d') . '_' . strtolower(get_class($this)); } /** * Returns the prefix for the message being added to the log * @param $currentLogBody * @return string */ protected function messagePrefix($currentLogBody) { return ($currentLogBody ? "\n" : "") . $this->logDateTimeStamp(); } /** * The prefixed date time stamp that is added before a log message * * @return string */ protected function logDateTimeStamp() { return Carbon::now()->toDateTimeString() . ' '; } }

And here is the implementation of that trait

<?php namespace App; use App\Flux\Components\LoggableTrait; class Cluster extends Base { use LoggableTrait; protected $table = 'clusters'; protected $fillable = ['name', 'active', 'status_code']; }

Pretty neat, huh? Here is the test class that brought this Logging feature about.

<?php use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseTransactions; class ClusterLogTest extends TestCase { use DatabaseTransactions; /** @test */ public function a_cluster_can_create_a_new_log_entry() { $logString = "some text here"; $cluster = factory(App\Cluster::class)->create(); $cluster->log($logString); $log = $cluster->currentLog(); $this->assertStringStartsNotWith("\n", $log->body); $this->assertStringEndsWith($logString, $log->body); } /** @test */ public function a_cluster_can_add_to_an_existing_log() { $logString = "some text here"; $logString2 = "some text here2"; $cluster = factory(App\Cluster::class)->create(); $cluster->log($logString); $cluster->log($logString2); $log = $cluster->currentLog(); $this->assertStringEndsWith($logString2, $log->body); } /** @test */ public function a_cluster_can_remove_all_logs() { $logString = "some text here"; $logString2 = "some text here2"; $logString3 = "some text here3"; $cluster = factory(App\Cluster::class)->create(); $cluster->log($logString); $cluster->log($logString2); $cluster->log($logString3); $cluster->clearLogs(); $log = $cluster->currentLog(); $this->assertEmpty($log); } }

Am I doing this right? I have no idea. What I do know is that I started out needing the ability to log and now I have a trait I can slap on any model to store logs on. Back in the day I would have extended a class to take on this functionality but this is so much better.

Imma keep at it. This was a great start.