Showing posts with label: laravel. Show all posts.

Active Record

Sunday 19 March 2017

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.

Labels: coding, laravel
No comments

Laravel Socialite

Thursday 02 March 2017

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.

Labels: coding, laravel
No comments

Dynamic Routing in Laravel

Tuesday 28 February 2017

LaravelI 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.

Labels: coding, laravel
No comments

AMP Form

Thursday 16 February 2017

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.

Labels: coding, laravel, amp
25 comments

AMP Authentication

Wednesday 15 February 2017

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:

  • Access-Control-Allow-Credentials: true
  • Access-Control-Expose-Headers: AMP-Access-Control-Allow-Source-Origin
  • Access-Control-Allow-Origin
  • AMP-Access-Control-Allow-Source-Origin

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.

Labels: coding, laravel, amp
No comments

Archives