Sept. 29, 2017, 12:10 p.m.
For a Laravel application I am working on the need arose to log errors to the database rather than to log files. I also wanted to show a custom error page instead of Laravel's default error page. This is my solution:
In app/Exceptions/Handler.php:
public function report(Exception $exception) { if(!config('app.debug')) { if ($this->shouldReport($exception)) { $this->logError($exception); } } if(env('APP_ENV') == 'local'){ parent::report($exception); } } public function render($request, Exception $exception) { if(!config('app.debug')) { if($this->shouldReport($exception)){ return response()->view('errors.500', compact('exception')); } } return parent::render($request, $exception); }
Function report checks to see if APP_ENV is set to local, if so it displays the error as normal. If it is not local it calls function logError which writes the error to the database.
Function render also checks APP_DEBUG and if it is set to true it reports the error to the screen as normal. If it is set to false it returns a custom error template which is stored in resources/views/error/500.blade.php.
Aug. 9, 2017, 1:21 p.m.
After spending yesterday figuring out how to write custom log files, today I changed my mind and went a different route. The problem with these log files is that they would need to be parsed to generate the reports I would need to generate. While this could easily and efficiently be done with something like Python, in order to stick with using PHP I decided to take a completely different approach.
I created a statistics table where I can just increment the appropriate column rather than writing a whole line to a text file. My concern with this was that it would down the site substantially, so rather than recording the data while the user is performing the searches I created a Job and queue it to be processed later. This doesn't help with the server load, but it does remove the response time for the user as a factor.
Aug. 8, 2017, 12:34 p.m.
Laravel comes with Monolog which it uses to log application exceptions and such. The need arose recently to log other information to text files, specifically searches performed by users. I tried logging this to the database but the table grew very quickly and performance suffered as a result.
So I decided to log to text files, which I can then process when reporting is needed. It would be relatively easy to use PHP file operations to open and append to a text file, but I decided to try to stick to using Monolog. Most of the information I was able to find online about how to do this involved overwriting Laravel's Logging Configuration classes to allow different types of data to be written to different log files, but it seemed like an awful lot of code to do something that should be relatively simple. After more searching I found a method that uses 4 lines of code and works perfectly:
$logPath = [path to log file] $orderLog = new Logger('searches'); $orderLog->pushHandler(new StreamHandler($logPath), Logger::INFO); $orderLog->info('Search : ', $data);
By including the date in the $logPath as part of the file name I can automatically rotate the logs and write to whatever location I choose to. The $data variable needs to be an array and is encoded to JSON and written to the log. The parameter passed into the new Logger() is the channel written to the log.
The only thing I have not yet been able to customize is the Priority - in this case "Info." For my purposes this isn't really necessary and could be ommitted, but having it doesn't have any downsides so I don't know if it's worth the trouble of figuring out how to remove it.
June 27, 2017, 9:21 a.m.
Over the last few weeks I have become enamored of using traits in PHP. Whereas I previously would put functions that need to be reused into either my Models or into helper functions, I have now started to make traits with these functions in them. For my Laravel application I created a directory app/Http/Traits where I keep my traits.
I started doing this when I began to optimize my code, trying to remove redundant code and remove unneccesary weight from my models. Using PHPStorm's very useful ability to find duplicated code I searched for blocks of 10 lines or more that were reused and moved those into traits. As I continued to do this I started to realize other benefits of using traits - mainly that it provides a way to simplify things. If the same action is taken in different controller or different parts of the application by using a Trait if I decide to change it I only have to change the code in one place rather than tracking down every place in the code that needs to be changed.
June 1, 2017, 9:34 a.m.
For the project I am working on we want to have multiple SMTP configurations in the database, which can be chosen at runtime. It's very easy to update the mail config using the config() helper, but for some reason that did not change the SMTP settings used to actually send the mail. I did a lot of research and found some answers for older versions of Laravel that did not work with 5.4.
Eventually I was able to find how to accomplish this from this post on Laravel.io. It seems that the mailer instance is created with the app, so updating the config won't change the properties of it which have already been set. To do that you need to use the following code:
extract(config('mail'));
$transport = \Swift_SmtpTransport::newInstance($host, $port);
// set encryption
if (isset($encryption)) $transport->setEncryption($encryption);
// set username and password
if (isset($username))
{
$transport->setUsername($username);
$transport->setPassword($password);
}
// set new swift mailer
Mail::setSwiftMailer(new \Swift_Mailer($transport));
// set from name and address
if (is_array($from) && isset($from['address']))
{
Mail::alwaysFrom($from['address'], $from['name']);
}
If you execute this after you have updated the config it will create a new instance of swift_mailer with the update mail config. Once that is done you can just send the mails and it will use the proper SMTP server.
May 15, 2017, 1:09 p.m.
For quite a while I have been struggling to get domain routing working in Laravel. Subdomain routing comes out of the box, and what I read said that adding domain routing should be fairly easy.
The first thing to do is get the full domain passed into the router. By default Laravel only takes what comes before the first ".", so to get the full domain passed in you need to add this to your Providers/RouteServiceProvider.php file:
Route::pattern('domain', '[a-z0-9.\-]+');
parent::boot();
Now you can access the full domain in the routes file, and you can do so by adding a route group:
Route::group(['domain' => '{domain}'], function ($domain) {
I tried to get the routes to take in the domain as a parameter and create themselves dynamically, but that did not work. So I ended up creating the route group. My issue was that some domains just use the normal route, and others were to have their own custom routes. I spent a while trying to get that working before just adding the route group. Inside the route group I check the database to see if this domain uses the normal routes or gets a special route.
The next issue was how to pass the variables from the routes to the controller, when they do not come from the URL. The special routes can be accessed via URL: http://www.maindomain.com/site/1. I wanted to be able to map via http://www.customdomain.com to that URL, but to do so I needed to pass the parameters from the URL into the controller when the parameters do not exist in the URL. That took some more figuring out, but it turns out you can do it like this:
$app = app(); $controller = $app->make('App\Http\Controllers\WhateverController'); return $controller->callAction('show', ['request' => $request, 'id' => $id]);
The controller expects a Request object, and to get that passed in you need to add it to the route explicity:
Route::get('/', function ($domain, \Illuminate\Http\Request $request) {
With that addition I am able to map the custom domain to a specific controller and pass in variables which are determined in the routes file.
The final code looks like this:
Route::group(['domain' => '{domain}'], function ($domain) { Route::get('/', function ($domain, \Illuminate\Http\Request $request) { $site = \App\Website::where('host', $domain)->first(); if($site){ $app = app(); $controller = $app->make('App\Http\Controllers\DomainController'); return $controller->callAction('show', ['request' => $request, 'id' => $site->store_id]); } else { $app = app(); $controller = $app->make('App\Http\Controllers\HomeController'); return $controller->callAction('index', []); } }); });
So for route "/" it checks to see if the domain exists in a table in the database, if so calls DomainController with a parameter from the DB and the request. If not it calls HomeController.
After a long time spent trying to figure this out it turns out to be a lot simpler than I thought it would be. However now I need to add specific routes to some domains but not others. I don't expect that will be too different than the method I am currently using.
May 15, 2017, 1:09 p.m.
For quite a while I have been struggling to get domain routing working in Laravel. Subdomain routing comes out of the box, and what I read said that adding domain routing should be fairly easy.
The first thing to do is get the full domain passed into the router. By default Laravel only takes what comes before the first ".", so to get the full domain passed in you need to add this to your Providers/RouteServiceProvider.php file:
Route::pattern('domain', '[a-z0-9.\-]+');
parent::boot();
Now you can access the full domain in the routes file, and you can do so by adding a route group:
Route::group(['domain' => '{domain}'], function ($domain) {
I tried to get the routes to take in the domain as a parameter and create themselves dynamically, but that did not work. So I ended up creating the route group. My issue was that some domains just use the normal route, and others were to have their own custom routes. I spent a while trying to get that working before just adding the route group. Inside the route group I check the database to see if this domain uses the normal routes or gets a special route.
The next issue was how to pass the variables from the routes to the controller, when they do not come from the URL. The special routes can be accessed via URL: http://www.maindomain.com/site/1. I wanted to be able to map via http://www.customdomain.com to that URL, but to do so I needed to pass the parameters from the URL into the controller when the parameters do not exist in the URL. That took some more figuring out, but it turns out you can do it like this:
$app = app(); $controller = $app->make('App\Http\Controllers\WhateverController'); return $controller->callAction('show', ['request' => $request, 'id' => $id]);
The controller expects a Request object, and to get that passed in you need to add it to the route explicity:
Route::get('/', function ($domain, \Illuminate\Http\Request $request) {
With that addition I am able to map the custom domain to a specific controller and pass in variables which are determined in the routes file.
The final code looks like this:
Route::group(['domain' => '{domain}'], function ($domain) { Route::get('/', function ($domain, \Illuminate\Http\Request $request) { $site = \App\Website::where('host', $domain)->first(); if($site){ $app = app(); $controller = $app->make('App\Http\Controllers\DomainController'); return $controller->callAction('show', ['request' => $request, 'id' => $site->store_id]); } else { $app = app(); $controller = $app->make('App\Http\Controllers\HomeController'); return $controller->callAction('index', []); } }); });
So for route "/" it checks to see if the domain exists in a table in the database, if so calls DomainController with a parameter from the DB and the request. If not it calls HomeController.
After a long time spent trying to figure this out it turns out to be a lot simpler than I thought it would be. However now I need to add specific routes to some domains but not others. I don't expect that will be too different than the method I am currently using.
May 7, 2017, 10:40 a.m.
One of my greatest frustrations with Eloquent collections has been that I needed to loop through the collection and either create a new collection or array if I wanted to have the results in a format where I could access them by a value in the query results. That is to say if I want to be able to access the results by say primary key, I would need to loop through the Collection returned by the query and create a new object with the key as whatever I wanted it to be.
I just learned that there is a much easier way to do this:
Model::all()->keyBy('whatever');
This will return the collection with "whatever" as the key, which makes life so much easier and code so much cleaner.
April 13, 2017, 3:56 p.m.
I tried a bunch of different things to try to address the issues I was having with using Socialite in a site that has multiple domains. The problem ended up being the session domain. I was able to generate callback URLs for each domain easily, but I couldn't get around the session domain issue.
Rather than spend more time on this I ended up using a little workaround. From my login page if you click on the login with whatever provider button it directs you to one domain, the one that the session domain is set to, and from there the Socialite logins work fine.
It's not ideal, but the worst that will happen is that someone ends up on a different domain than they started on.
April 7, 2017, 4:29 p.m.
I've been struggling with an error with Laravel Socialite for months now. At times the socialite login worked perfectly, but at other times and with certain providers I got an error:
InvalidStateException in AbstractProvider.php line 200
This was not critical functionality so I just kept pushing it off, but finally we have found the solution. It is related to the domain in config/session.php. This value defaults to NULL, and it apparently needs to be set to the domain the site is running on. This site in question runs on many domains, so setting the value to a single domain fixes one domain but leaves all the rest broken.
So for me the issue is not solved, but at least I am not seeing that error anymore.
March 20, 2017, 7:27 a.m.
When a friend first told me about Doctrine many, many years ago I thought it sounded like a terrible idea. I have also had a love affair with SQL and the thought of having to use objects and functions to interact with the database instead of writing queries and getting the data you needed was overly complicated and didn't add anything. When I first started using frameworks to code I felt the same way about them - it seemed to add unneeded layers of complexity to have to incorporate code written by someone else to do something I could very easily do on my own.
It wasn't until I started using Laravel that I came to appreciate frameworks - I could write a simple CRUD that would have taken me days writing from scratch in a couple of hours thanks to the functionality built into the framework. And the ORM made things a lot easier as far as database interaction, but only within certain parameters. I realized this when I tried to normalize a table in my database by separating out a field into another related table. It became very complicated and convoluted to do simple things like sort the query by a value in the related table. I ended up denormalizing the database and getting rid of the extra table to keep the code clean.
At the time I assumed that when you set up a relationship between models when you queried one Laravel would join the other tables in to get all the needed data in one query. It wasn't until I installed debugbar that I realized that laravel would do n+1 queries to get n rows of data with one relationship. This issue is avoidable using Laravel's "eager loading" - which will get the same data in 2 queries. However to get data from multiple tables in one query - using a JOIN - and specifying WHEREs and ORDER BYs on the joined tables, the Query Builder syntax gets quite ugly.
In my opinion this is largely because Eloquent is an implementation of the Active Record pattern, which represents one row of one table in the database as an object as far as the code is concerned. In Eloquent, if you query multiple rows you are returned a Collection of these model objects. While Active Record is great for dealing with simple databases where one row contains usable data, if you are dealing with a highly relational database where you need data from multiple tables it doesn't hold up so well.
When I started out programming we were using MS SQL Server and the programmers were not allowed to write any queries - we were to use stored procedures written by the DBA. At the time I didn't understand the reason for this, but now I realize that it allows the database structure to be separate from the code - so that database changes won't result in needing to rewrite large sections of code. This, in my opinion, is the main advantage of using an ORM. So what do you do when you need to write actual SQL queries for your code to work properly and efficiently?
One option I investigated was adding a Repository layer to the code. With the repository pattern the models handle the reading from and writing to the database, but the repository interacts with the code. For my needs, the repository would basically act the part of the SPs - the queries would be written in them and the code would call the methods in the Repository to get the data they needed. Basically it just put all of the queries in one place so that if the database was ever changed the queries that needed to be rewritten would all be in one place. I tried implementing this, and it worked, but it added another level of abstraction and complexity. And the way I implemented it was basically no different from writing the queries directly into the models, which works just as well, but for some reason it bothers me to have complex and bloated models.
I am still working through this issue and do not have a solution yet. Dealing with this has made me remember the many issues I had with ORMs and frameworks in general back when I first started using them. Using the tools of the ORM the issues can be addressed - but not in a simple, clean and elegant manner. And in my opinion, the main problem is the Active Record pattern itself - it is great if you need to work with a single row in a single table, but if you need to span multiple tables to get your data, it doesn't hold up so well.
March 5, 2017, 8:07 a.m.
Once I had the authorization working and could display the comment form properly the next step was to get the form submitting properly. First of all, to use forms in AMP you need to include the following script:
<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>
AMP forms work mostly the same way as normal HTML5 forms, but there are a few differences. With AMP you have to either use GET forms or you can use POST forms but you need to use action-xhr instead of action. For POST forms you also need to include a target of either _blank or _top. Using action-xhr means that the page will basically post an AJAX request which expects a JSON response instead of reloading the whole page. If you want to reload the whole page you should use method="get".
The response from the request needs to include the same headers as the authorization request, which are detailed in this post. The response doesn't need to contain any specific data, you can put whatever you want in there. You can use the response data to update the page after a successful post, but I have not done that yet.
In this case, the controller function action for my new comment just adds the comment and then returns the id and body of the new comment in JSON along with the necessary headers. I would like to display the new comment on the page, but the syntax to display the JSON data is {{ var }} which is the same syntax blade uses. I know there is a workaround for this, but I haven't looked for it yet.
The two big issues I had with this form were how to update the page after a successful post. I wanted to do two things: hide the form to leave a comment and display a success message. There are AMP components to accomplish both of these tasks.
To hide the form after a successful post you can add the following CSS:
form.amp-form-submit-success > input {
display: none
}
And set the form's class to "hide-inputs." When the success message comes back the form's class is updated to included amp-form-submit-success which will cascade down to hide any child inputs. I had my inputs in a panel inside the form and the inputs were not hidden because the input was not a direct child of the form. This was fixed by rearranging the elements so that the form was inside the divs so that the inputs were children of the form. Before I arrived at this solution I first tried to hide the entire form, which worked great, but also hid the success message. Since my form field was a textarea I had to add another item to the CSS for textareas, otherwise identical to that above.
The next step was displaying a success message, which I did by following the instructions here. The block of sample code from this page is:
<form class="hide-inputs" method="post" action-xhr="https://ampbyexample.com/components/amp-form/submit-form-input-text-xhr" target="_top">
<input type="text" class="data-input" name="name" placeholder="Name..." required>
<input type="submit" value="Subscribe" class="button button-primary">
<div submit-success>
<template type="amp-mustache">
Success! Thanks {{name}} for trying the
<code>amp-form</code> demo! Try to insert the word "error" as a name input in the form to see how
<code>amp-form</code> handles errors.
</template>
</div>
<div submit-error>
<template type="amp-mustache">
Error! Thanks {{name}} for trying the
<code>amp-form</code> demo with an error response.
</template>
</div>
</form>
The div submit-success is hidden until the form submission comes back with a success message at which point it is display, and likewise with the submit-error block. To use the amp-mustache templates you need to include the following in a script tag:
script custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.1.js"
In the submit-success section the {{ name }} will substitute in the "name" element from the JSON data returned from the post. In my case I have left this out for now and just display a success message and hide the form.
To see this in action you can look at the AMP version of this blog here. When you submit a comment the form disappears and is replaced by a success message. Ideally the new comment would show up, but I'll get to that at some point in the future.
Update - the blade syntax to display the "{{ }}" for Javascript is "@{{ whatever }}". So I updated my code to actually display the comment after it is posted.
March 4, 2017, 2:04 p.m.
I've been trying to figure out how to make packages for Laravel, and there isn't as much documentation as one would hope there is. The Laravel docs aren't as helpful as they could be for someone who has never done this before, and most of the info I found on Google was either incomplete or for older versions of Laravel.
I did find a few pages with helpful information on how to do this, this one is the one I followed. It uses this CLI tool, itself a Laravel package, which will allow you to make other Laravel packages. The CLI tool creates the directory structure along with composer.json and boilerplate code that provides a good starting point.
Other tutorials I found helpful include:
I ran into a few problems which took some research to solve, which I thought I'd put here in case anyone else is having the same issues:
Of course I had other issues but those are the ones that took a while to figure out. I hope to finish the package up in the next few days, I'll post updates as they come.
Update - to use action() to create URLs you in fact do use the full path to the controller and it works this time. Not sure what I did wrong last time, but it is working fine now.
March 4, 2017, 2:04 p.m.
I finished working on my Laravel package, which is the blog I use here (and also on my other site). I had this on my GitHub as a Laravel skeleton application, but after a few days of research and coding I now have it as a Laravel package, which can be installed via Composer. I did find a more comprehensive tutorial on writing Laravel packages, but I only just found this today after I had finished my package, so haven't really read through it.
The reason I started working on this package is because I have multiple sites that use the same code and I wanted to consolidate them so I wouldn't have to maintain two separate code bases, but the package is only in English and some of my sites are in French, so I guess my next step is adding translation to the package.
The package is on Packagist and can be installed with composer.
composer require escuccim/larablog
A few things that I struggled with and eventually figured out since my last post on this topic:
$this->publishes([
__DIR__.'/config/config.php' => config_path('blog.php'),
__DIR__ . '/resources/views' => base_path('resources/views/vendor/escuccim')
]);
$this->mergeConfigFrom(__DIR__.'/config/config.php', 'blog'); where 'blog' is the key for the config array.
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
There is still work to be done, but I just marked my GitHub repo with a stable release version, so that's something.
March 4, 2017, 2:04 p.m.
Apparently Google doesn't like it if all of your pages have the same title and meta description tags. So yesterday I decided to write unique titles and description tags for all of my pages. At first I did this by setting two variables - $title and $description - in my controllers and then passing them to the views, where I displayed them in my layout/app.blade.php. Since I have multiple languages in this site I ended up setting them like this:
$title = trans('whatever.pagetitle');
$description = trans('whatever.pagedescription');
This seemed a bit inelegant and I thought I could come up with a better way, which I did this morning. What I did was set up a file in my lang directory I called metadata.php. A sample of this is here. This file contains for each page a key for title and description as follows:
'/home-title' => 'Title';
'/home-description' => 'Description';
By using the URI appended with the value I want I was able to consolidate all of the values into one file for ease of use, and I was also able to make a helper function to get those values from the translation files and display it, so that the same exact code could be run on every single page and return the data I need.
The helper function I used is on my GitHub here, and if it doesn't find data for the page it is looking for it has a default title and description it uses. For pages like blog articles and individual records I use the same title and description, but I still specify $title in the Controller, and if the value exists it is appended to the title in the layout file.
I like this solution because it allowed me to delete the redundant and ugly code in the controllers where I specified a title and description for each page with a function that pulls the data from one location, and if the data doesn't exist it substitutes a default in, instead of either failing or not doing anything. The code I used is on my GitHub Gist.
March 4, 2017, 2:04 p.m.
After having written a few packages I believe I now have it down cold. I started another one just a few hours ago and am already finished with it and had no problems at all this time. The latest package is escuccim/recordcollection which is a package with the code I use for my searchable database of vinyl records. Yesterday I did a package with the code for my online CV in it, so at this point this site is basically just the static pages and four Laravel packages. While it's a bit more complicated to make changes now because I need to go to the package code, alter it, then push it up to git and then composer update, I think the extra couple steps is well worth it in terms of maintainability and portability. My packages are largely self-contained, with their own views, controllers, models, etc. so I can just add them to a project and everything will (almost) magically work.
I don't really see much demand for a package like this for other people so I haven't thoroughly tested this one in projects other than my own, so it may not work properly out of the box. If you have a large record collection that you want to store online I would recommend Discogs.com. It has a lot more features and functions than my package does, but I don't have the time to go through each of my 2,000 or so records and add them to my Discogs collection and I've had most of mine in a database for about 15 years, so I'm sticking with my own for now. If I was starting over from scratch I'd probably put it in Discogs and then pull the data from their API to display it here.
My other packages are all tested and working on fresh installs of Laravel, so feel free to use them if you want.
Update - I ended up testing the recordcollection package on an almost fresh install of Laravel and I fixed the issues I found, so it should be in mostly usable shape.
March 4, 2017, 2:04 p.m.
Back when I wrote the code to localize this site I ran into some unexpected behavior that I couldn't figure out. I have two ways to set the language here - first you can do it by subdomain, fr defaults to French. Then regardless of the subdomain you can use the drop-down menu in the navbar to set the language, which sets a session variable. To handle the subdomain I have a middleware which runs on every request and sets the locale to the language specified by the subdomain, if a subdomain is used. I was confused by why the subdomain could be overwritten by the session var set with the drop-down menu, but I ended up leaving it that way because it worked better than the way I had originally envisioned.
This weekend I decided to try to get to the bottom of why the behavior was different than what I would have expected and I discovered something a bit bizarre about Laravel sessions. In the middleware the session is always empty, but I can set a variable and access it from within the middleware. By the time I get to the controller the values put in session in the middleware are gone, and replaced with the values previously set in the session. I haven't looked at Laravel's session code yet, but I assume that however it stores session variables is initialized somewhere between the middleware and the controller. Before I started with Laravel, I used to keep session variables in $_SESSION, so the way it works now is a bit confusing to me.
To explain, I have the following in my middleware, which is registered to run on every request:
public function handle($request, Closure $next)
{
echo "1: " . session('foo') . "
";
session(['foo' => 'bar']);
echo "2: " . session('foo') . "
";
return $next($request);
}
When I load any page, it outputs:
1:
2: bar
If I then in a controller execute:
session(['foo' => 'baz']);
And load another page which just contains:
echo "3. " . session('foo');
The output, with the middleware is:
1.
2. bar
3. baz
So, in the middleware you can set and access the session, but the session doesn't persist past the request, and by the time the controller is executed that session has been replaced with a session that does persist from the previous request. I can think of a few ways around this, but it doesn't seem worth the effort involved. For me, the result of this issue is that I have to include a call to a helper function in every single page I want to translate - if I want to keep the drop-down menu to translate. My other option would be to just do the localization based on subdomain and have the drop-down menu link to the same page on a different subdomain instead of just setting a variable and reloading the same page, which may in fact be a better solution, but again maybe not worth the effort.
I don't know if anyone else has run into this behavior in Laravel, I also don't know if this behavior is intentional or not, but if you are trying to access or set session variables in a middleware with no luck this is likely the reason.
March 4, 2017, 2:04 p.m.
I figured out how to resolve the session and middleware issues I mentioned in the previous post. I previously had the middleware in the $middleware array in /app/Http/Kernel.php. The Laravel session is started with the middleware StartSession, which is in the $middlewareGroups array under web. I moved my middleware to the web $middlewareGroups and put it after StartSession, and now the Middleware can access the session. The only difference is that the Middleware will only be run on requests that are part of the 'web' group instead of on every request, but this actually makes more sense in this case.
After having figured this out the unexpected behavior I was witnessing before makes sense now.
March 4, 2017, 2:04 p.m.
I just upgraded one of my projects to Laravel 5.4 and I immediately had some issues with PHPUnit tests. They changed the testing framework in the new release, and you will need to alter any existing tests that use browser testing accordingly. This is all documented in the upgrade notes under the testing section.
After making the changes listed in the documentation most of my tests ran fine, but I still had one that kept giving me an error I couldn't figure out:
Fatal error: Class 'BrowserKitTest' not found
I copied over the code from tests that were working into the file with the error and the exact same code that was working in one file was giving me this error in a file with a different name, which was very frustrating, to say the least. After a little bit of trial and error I came up with a way to fix this issue, which was simply to rename the file. The original name of the file was BlogTest.php, but when I rename it to CBlogTest.php it works fine. My guess as to why this is is that it loads the php files in the /tests directory in alphabetical order and it couldn't find the class BrowserKitTest until it had loaded that file. I assume this is because I have upgraded from 5.3, and the new BrowserKitTest.php file needs to be added to some autoload file somewhere.
I'll post more thoughts about Laravel 5.4 after I've had some time to mess around with it. So far, other than this one issue, all my code has worked fine after upgrading.
March 4, 2017, 2:04 p.m.
Yesterday, after playing around with Laravel 5.4 for a few days on my dev environment, I upgraded this site. The only issue I had was with the testing, which I addressed in the previous post. The PHPUnit issues were easily resolved by installing laravel/browser-kit-testing and updating my existing tests to reference the new BrowserKitTest.php class instead of the old TestCase.php. The issue of the naming of the test files was in fact due to autoloading, and I resolved it by adding the following to my composer.json under the "autoload-dev" classmap section:
"tests/BrowserKitTest.php"
After upgrading from 5.3 to 5.4 you need to clear your view cache, which you can do with:
php artisan view:clear
And the upgrade guide also suggests clearing the route cache with:
php artisan route:clear
I have never had an issue with the route cache, but I have often had issues with the view cache, so I personally clear my views after most of my updates.
The other thing to be aware of when upgrading to 5.4 is that tinker is no longer part of Laravel, so needs to be installed separately as laravel/tinker. I believe it is installed by default with a new installation of 5.4, but if you are upgrading you need to do this, especially if you use tinker as frequently as I do.
I haven't used any of the new features of 5.4 yet, from reading the upgrade guide there aren't really many features that jump out at me as something I would make a lot of use of, but I'm sure I will in the future.
March 4, 2017, 2:04 p.m.
I made an update the other day to the LaraBlog package. I added the ability to reply to comments and for users to delete their own comments. At first I had a hard time figuring out how to display the nested comments, as theoretically you can have an infinite number of replies to a comment, so how do you determine the inset when Bootstrap only has 12 columns?
This was easily resolved by two realizations:
So what I did was update my Blog model so that when getting the comments for a blog I only get the original comments, not replies. Then I added to my Comments model a function to get the replies to a specific comment.
I already had my comments display in a separate view which displayed the form to leave a comment and then all of the comments for a specific post. I separated this out into multiple views:
The index, when including replies, leaves a blank column to the left of the replies, which, when recursively included in the levels of nested replies, will size itself to the available space, so that the indent gets smaller on every nested level of replies - but there could theoretically be an infinite level of nested replies without breaking anything.
I had never thought of recursive including of views until I came across the idea on stack overflow while looking for a solution as to how to address what looked to be a nightmare of infinite nested loops; but that idea provided a simple and elegant solution to what was looking to be a complete mess.
March 4, 2017, 1:03 p.m.
Over the last week I have been messing around with adding Structured Data to my pages so that Google can display Rich Cards. Google hasn't yet indexed my pages with structured data so there's not much I can say about that so far. I have also been playing with Accelerated Mobile Pages (AMP), which are lightweight pages designed specifically for mobile devices.
The two resources for AMP which I've found to be useful are AMP Project and AMP By Example. Unfortunately neither of them goes into a whole lot of detail about how to implement this stuff and I haven't been able to find very good explanations online. However I've been able to solve most of the issues I've encountered by trial and error.
The biggest difference between AMP and normal HTML is that AMP does not allow Javascript, nor does it allow linked CSS. All CSS must be inline, must total less than 50kB, and the only Javascript you can use is special Javascript from AMP Project. All images must have sizes defined and forms work a bit differently. The reason for this is to avoid any blocking resources that could slow the load of the page. I have been using Bootstrap CSS which I thought would be compatible as it is responsive and displays great on mobile devices, but it's too big and uses Javascript. I ended up using Bootstrap's Customizer to only output the elements I needed and then minified that and included it into my page. At some point I will clean out unused styles from the CSS to trim it down even more, but I was able to get my CSS to just barely fit the maximum size requirements by only using the bare minimum.
AMP isn't really all that complicated - it really just restricts what you can use in the page, but there were a couple things that I really struggled with. Those were, in order of difficulty:
I will post more articles on each of these three issues and my solutions to them over the next few days, as I get the kinks worked out.
March 4, 2017, 1:02 p.m.
Many years ago, before cloud servers existed, I worked for an ISP. We were running Linux servers and we had way more problems with them than one would think possible. It seemed like every other month the servers would crash and we would lost most of the data on them. Back then we had a tape backup system, which seemed almost as failure prone as the servers, so when the crashes happened there was rarely any recoverable data.
As a result of that experience I am meticulous about always having my important data backed up. All my code is either on GitHub or BitBucket, so the only thing that exists only on my servers is my databases. A couple of months ago I decided I needed to have that data backed up regularly and after considering a few options I decided to back it up to Amazon S3. I wrote a two piece solution consisting of:
Since the PHP part was a command I could just call it from my shell script and schedule that as a cron job. Yesterday I decided to turn the Laravel piece of that into a package called escuccim/s3backup. The code was originally written specifically to upload my DB dumps to S3 and had most parameters hard-coded in, so I added a few options to the command and updated my shell script to pass them in.
The package currently only works for a single file at a time, as that is all I need it to do, but I may add support for directories at some point in the future. The package is available through packagist although to use it you currently need to specify version dev-master.
March 1, 2017, noon
When I worked on this site I implemented a "login with Google" feature for which I used Google's Authentication API. But I used it manually. I used Google's Javascript function and wrote a controller to handle the data the API returns. It works, but it's a bit clunky and far from ideal.
Just today I used Laravel's Socialite package for the first time. It can handle Oauth requests for Google, LinkedIn, Twitter, Facebook, GitHub and BitBucket - and it's much, much easier to use than it was doing it myself. When I was looking into using Oauth I found a Laravel package to integrate Oauth logins, but it was very complicated to use. It created about a dozen tables and I ended up abandoning it to write my own code for integrating with Google.
With Socialite all you do is put the Client ID and the Secret's into a config file and add two functions into your LoginController - one to handle the login attempt and one to handle the callback. The login function just directs the attempt to the appropriate provider:
return Socialite::driver($provider)->redirect();
And the callback function gets the information returned by the provider:
$user = Socialite::driver($provider)->user();
In the callback function I also handle adding the user to my database and logging them in. Next chance I get I'm going to take out all of my Google Javascript code from this site and replace it with Socialite. I couldn't believe how simple it was.
Feb. 28, 2017, 5:09 p.m.
As a result of figuring out what was going on with the session and the middleware yesterday I was able to rewrite my localization code and greatly simplify the whole project. Previously I had been calling a function to set the language in every single controller action that returned a view, I was able to eliminate all of that and consolidate everything in the middleware.
I made another package - escuccim/translate - that has two parts:
First is the middleware which does two things:
a. Checks the subdomain to see if the subdomain corresponds to a language. If so it sets the app locale to the appropriate language.
b. Checks to see if there is a session variable with the language in it, if so it sets the app locale accordingly.
The key for me here is that if there is a locale specified by both the subdomain and the session, the session takes precedence, thus allowing the user to display the page in whatever language they desire, irregardless of the subdomain.
The second component of the package is a route which accepts a locale as a parameter and sets a session variable to that locale, so that the middleware can then access that information.
This package is available on my GitHub and my Packagist. When I am done testing it you can install it via composer.
I'm glad I took the time to investigate the session/middleware issues because figuring that out allowed me to replace code that was unneccessary and ugly to look at with a nice, simple, elegant solution.
Labels: coding , laravel , localization
Feb. 28, 2017, 5:02 p.m.
Since I have multiple sites that use almost the same code I have been trying to consolidate shared code into Laravel packages for ease of maintenance. This weekend I did my second package which is escuccim/sitemap which contains my code for generating XML sitemaps for Google. Since I have this site available in more than one language and I use subdomains to set the default language it made for very messy and confusing hardcoded sitemaps. I was able to shrink the code for each sitemap down from hundreds of lines to about 50 by putting the subdomains and the corresponding language in a DB table and then looping through them to output the URLs and hreflang tags in the sitemap. This time the process of writing the package was quick and easy using the same method that I struggled with last time.
Once I had that working I went back to my LaraBlog package which I added translation functionality to. I had one big problem which took me a while to figure out which was that it wasn't loading the translations at all, it was just displaying the key: 'escuccim::blog.key'. I researched this and found no answers, but was able to solve it by changing the namespace or hint to larablog. I am not sure why this worked, but I suspect it may be because I was using the namespace escuccim for the views and maybe they conflicted? Anyway if anyone else is having this issue try to change the namespace/hint.
When I had the blog package translating properly I deleted the code I was using for this site for the blog and the sitemap and replaced it with the new packages. So far everything seems fine, but I will give it a day or two before to turn up any issues before I start using the packages in other places.
I have a few other things I want to put into packages, and I just have to say that Composer makes my life so much easier! Instead of having to go through my code line by line to copy changes from one place to another while avoiding any functionality that differs from one project to the next I just update the package and then composer does the rest!
Feb. 28, 2017, 4:47 p.m.
I was just dealing with an issue where I wanted to create routes from the database. The site has pages which are contained in a database table and I had a route which took in the name or id as a parameter and rendered the appropriate page. Of course it doesn't really look nice if you have to go to /pages/about, a more intuitive way would be just /about, so I was trying to figure out how to accomplish that.
I tried getting the pages from the database and creating the routes dynamically, but that wasn't working because the route still needs to pass a parameter to the controller. I could have gotten the URL from within the controller and used that, but I found an easier and cleaner way.
At the very end of the web.php routes file I added:
Route::get('/{slug}', 'PagesController@show');
When Laravel has a route it goes through the file and tries to find a match. When it finds one it stops and executes it. So by having this route at the end of the file it will only match routes that haven't already been matched. So for any route that isn't already defined it will called PagesController@show and pass it $slug, which is the exact same thing that the old route did:
Route::get('/pages/{slug}', 'PagesController@show');
Except this route gives me a nice, clean URI instead of a clunky, ugly one.
Feb. 14, 2017, 2:34 p.m.
I finally got the AMP forms working as expected. It was a bit tricky to figure out so I will outline the issues I encountered and how I solved them. The situation I was working with was making a comments form for the AMP version of my blog pages.
The first issue I had to deal with was that a user can't leave a comment unless they are logged in. In the rest of the app I use the session to determine if the user is logged in, but AMP has it's own protocol for doing that, which involves making AJAX requests to a page which returns a JSON response to determine if the user is logged in. In this case, in the controller I simply do an Auth::check() and return a JSON response depending on the results of the check().
The issues arose from the fact that AMP requires specific response headers, which took me a while to figure out how to set properly. I wasn't able to find much documentation on the values of these headers, but I was able to figure out the proper values.
The headers required were:
The latter two headers need to have specific values, and although they ended up being the same in most cases, I set them to the separate values to make sure errors won't occur.
The value for Access-Control-Allow-Origin needs to be the "origin" header made in the request, which I get with:
$request->header('origin')
The value for the AMP-Access-Control-Allow-Source-Origin needs reflect the value passed in the URL to the request, which is a parameter named: __amp_source_origin.
The authorization page can return a variety of values to indicate whether the user has a subscription, if they can view a specific number of free articles, and what they have access to. But in my case all I need to know is whether they are logged in or not, so I just return the JSON data:
{loggedIn: true}
To enable content being displayed differently based on authorization you need to include the following scripts:
<script async custom-element="amp-access" src="https://cdn.ampproject.org/v0/amp-access-0.1.js"></script>
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
You also need to include the following in a script to tell the scripts what to do and where to get the info from:
{
"authorization": "[Auth URI]",
"noPingback": "true",
"login": {
"sign-in": "[Login URI]",
"sign-out": "[Logout URI]"
},
"authorizationFallbackResponse": {
"error": true,
"loggedIn": false
}
}
Where [Auth URI] is the URI detailed above which returns whether the user is logged in or not; [login URI] is the URI to allow the user to login; and [logout URI] is the URI to allow the user to logout. All URIs must either be HTTPS or // or AMP will complain about them and won't function properly.
Then the following code is included in the template:
<span amp-access="NOT loggedIn" role="button" tabindex="0" amp-access-hide>
<button on="tap:amp-access.login-sign-in" class="btn btn-xs btn-primary comment-button">Login</button>
Please login to comment<br><BR>
</span>
<span amp-access="loggedIn">
@include('amp._commentForm')
</span>
The amp-access attribute in the span tells the page NOT to display the section if the user is loggedIn - presumably you could vary this to reference other data returned by the auth page. The on attribute of the button tells the page to reference the login:sign-in attribute of the amp-access script when it is tapped, so it will launch the [login URI] when the button is clicked. And finally the amp-access="loggedIn" attribute says that if the user IS logged in the commentForm will be included.
For me the most complicated part was figuring out the response headers required and their values, once I got that figured out the rest worked pretty easily. The next step was getting the actual form to submit and update the page properly. I'll write about that in the next post.
Feb. 12, 2017, 12:08 p.m.
I was having a hard time with the Laravel auth package. If you use the out-of-the-box Laravel Auth, if you try to access a page you don't have access to the Auth will redirect you to the login page and then after a successful login redirect you back to the page you were trying to access.
This works fine. But if I go to a page and then click on login it would redirect me back to a page specified in the Auth controller instead of to the page I was on before I clicked login. I searched for a while and found some info, but not much addressing this specific issue.
I finally found this thread on Laracasts which gives a simple and easy solution to the problem.
The solution is to override the login form method in the LoginController.php in the app directory. I added this function:
public function showLoginForm(){
if(!session()->has('url.intended')){
session()->put('url.intended', url()->previous());
}
return view('auth.login');
}
This pushes the previous page onto the session as url.intended, which is the same thing the middleware does. But this does it in all cases, not just when the middleware catches an auth error. After login the Auth controllers now send you back to url.intended instead of to the default page specified in $redirectTo.
Feb. 12, 2017, 12:08 p.m.
I just updated the search of my records here so it would load the results using Ajax instead of refreshing the whole page. Everything seemed to work fine, but then I noticed that it broke the Laravel pagination. I included the pagination in the section of the page reloaded by searches and sorts so it would update appropriately, but then the page links just loaded the results and didn't load the results into the div on the page where they were supposed to be.
This was a bit tricky to solve. What I ended up doing was exporting the pagination views to my resources directory, and then editing it there. To each of the pagination links I added two things:
Then I added a script to the page that triggers when you click an object with the class of page-link, that gets the page to be loaded from the data-val attribute and submits that to the script that loads the appropriate page with the appropriate variables. Then I did the same thing for the sort links - instead of having each trigger it's own script I made one script triggered by a click on the class and put the data in the data-val.
Feb. 12, 2017, 12:01 p.m.
I decided to try to translate this site into French, given that I live in the French-speaking part of Switzerland. Laravel has a lot of great tools for localization built-in, but there were a few things sorely lacking. Laravel, by default, has localization files in /resources/lang/en. Each file is just an array with a key and the translated text as the value. If you want to add a new language you just copy the files over into a new directory, in this case /fr, and translate the text directly in there. In the views instead of typing in the text directly you call trans('file.key') and it pulls the text for 'key' out of the 'file.php' in the appropriate language directory. This couldn't be any easier.
The hard part was when I started trying to figure out how to set the language to be displayed automatically. Laravel pulls this value from config/app.php, and you can change this value easily, but because Laravel is RESTful it has to be done on every request. So I decided to stick the actual language in a session variable and then change the value in the config array if needed.
I tried to make a middleware to do this on each request, but this didn't work because it seemed as if the session either wasn't saving from middleware, or possibly wasn't initialized yet in the middleware. More on this below. So I abandoned the middleware route and added a function in my controller that sets the language thusly:
App::setLocale( session('locale') ? session('locale') : config('app.locale'));
This worked fine, I just need to make sure to call the function everytime a page may need to be translated. My next step was to try to add a subdomain 'fr.' that would automatically set the language to French. You can do this in the web.php routes file, but from what I can tell it needs to be called on every single route, which seemed like an awful lot of work for something that should be pretty easy.
So I went back to the middleware and created a middleware called SetLanguage that I added to app\Http\Kernel.php so it runs on every request. The middleware is quite simply this:
$pieces = explode('.', $request->getHost());
if($pieces[0] == 'fr'){
session(['locale' => 'fr']);
}
return $next($request);
And this works fine. I think the problems I had with the middleware the first time I tried it was that Laravel has changed how sessions are handled in new releases, and the Session facade no longer works or works differently. Instead you now use the session() helper function or call $request->session() to modify the session. I had been trying to use the Session facade.
One thing that seems a bit odd is that since the middleware runs on every request it should translate every page that is called from the fr. subdomain to French. In actuallity it initially sets the language to French, but if you change the language using the drop-down menu it keeps the selection. This doesn't seem right, but in this case the actual behavior makes more sense than the expected behavior, so I am ignoring this bug.
Once I got this figured out, the translation was a simple matter of replacing text in my views with references to the lang files, which went smoothly, although I did have to spend some time trying to figure out how to translate some technical terms into French, and still am not sure I have them all translated properly.
Labels: coding , laravel , localization