DC Blog Blog RSS Feed https://dcblog.dev Laravel Sent Emails Package

I've released a new package for recording and displaying sent emails from Laravel applications.

The package will store all outgoing emails into a database automatically. The package also provides a user interface to display sent emails.

Watch a video walkthrough:

Check out the package Laravel Sent Emails

]]>
Sun, 12 Jul 2020 21:49:45 GMT https://dcblog.dev/laravel-sent-emails-package https://dcblog.dev/laravel-sent-emails-package
Laravel Blade Components Package

I've released a blade components package.

Components can be made in projects easily enough but often its good to reuse components across projects, this is where Laravel Blade components collection comes in.

The first collection I've added is for forms. There are components for:

  • form tags
  • inputs
  • textareas
  • checkboxes
  • radios
  • selects
  • buttons

These allow you to write forms with less markup required, for instance, take the example login form:

<x-form.open action='login'>

<x-form.input name='username' class='form-control'>{{ request('username') }}</x-form.input>
<x-form.input name='password' type='password' class='form-control'></x-form.input>

<x-form.button class='btn btn-sm btn-info mt-3'>x Login</x-form.button>

</x-form.open>

Another example, populate forms for a typical edit form:

<x-form.open method='put' action='{{ route('post.update')}}'>

<x-form.input name='title' class='form-input'>{{ old('title', $post->title) }}</x-form.input>

<x-form.textarea name='content' class='form-input' cols='10' rows='10'>{{ old('content', $post->content) }}</x-form.textarea>

<x-form.button>x Login</x-form.button>

</x-form.open>

Check out the Laravel Blade components package.

]]>
Sat, 11 Jul 2020 18:52:51 GMT https://dcblog.dev/laravel-blade-components-package https://dcblog.dev/laravel-blade-components-package
Laravel load anonymous components from packages

From Laravel 7 blade components are included. You may want to use these in packages.

For the components to be loaded from a package, the package service provider has to first load views from a folder and then use Blade::component to load the components.

Example:

In the package src folder create a folder called components

This example is for a fictional package called Elements

The service provider loads the files from the components folder and then uses Blade::component to load each one.

At the time of writing this, there is no way to automatically load components, the need to be loaded individually.

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;

class ElementsServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->loadViewsFrom(__DIR__ . '/components', 'elements');

        Blade::component('elements::form-button', 'form-button');
    }
}

These components can then use using by called <x- followed by the name:

<x-form-button method="GET" action="/demo">
    Delete
</x-form-button>
]]>
Fri, 10 Jul 2020 23:14:49 GMT https://dcblog.dev/laravel-load-anonymous-components-from-packages https://dcblog.dev/laravel-load-anonymous-components-from-packages
ASCII to Binary in PHP

Recently I inherited a project where all file attachments were stored in a database table in the format of 0xFFD8FFE000104A4649 I need PHP to be able to read and serve this data.

My first thought was to try using base64_decode in case this was in a base64 encoding format. It wasn't.

Turns out it's a Ascii format.

Solution 1

The below block converts the data into a format PHP can read by issuing a shell command xxd with -r and -p options passing the command to a proc_open command to run and then using fwrite to return the data to $source

$file = '0xFFD8FFE000104A4649...'; //var containing the source data
$desc = array(
    0 => array('pipe', 'r'), // 0 is STDIN for process
    1 => array('pipe', 'w') // 1 is STDOUT for process
);
$cmd = "xxd -r -p -";
$p = proc_open($cmd, $desc, $pipes);
fwrite($pipes[0], $file);
fclose($pipes[0]);
$source = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($p);

Whilst this works it does rely on being able to run shell commands on your server.

Solution 2

Another way is to use hex2bin function and remove the 0x characters from the data, this essentially does the same thing but the hex2bin does the heavy lifting.

$source = hex2bin(ltrim(rtrim($file), '0x'));

If you working with an image you can then output the image using base64 encoding:

base64_encode($source)

Or use headers to render directly:


header('Content-type: image/jpg');    
header('Content-Length: ' . strlen($source));    
echo base64_encode($source);
]]>
Wed, 08 Jul 2020 12:03:36 GMT https://dcblog.dev/ascii-to-binary-in-php https://dcblog.dev/ascii-to-binary-in-php
My favourite VS Code extensions

The following are the extensions I always install on VS Code, these are my preferred themes, snippets, and plugins.

Atom One Dark Theme

Atom One Dark Theme

I've tried lots of themes but Atom One Dark is my go-to theme. It's got a great mix of colours to differentiate between elements.

GitHub Theme

GitHub Theme

GitHub theme is one of the best light themes I've come across.

Auto Rename Tag

Auto Rename Tag

Automatically rename paired HTML/XML tags, the same as Visual Studio IDE does.

Better Align

Better Align

Align your code by colon(:), perfect for ensuring all your array keys line up perfectly.

DocBlocker

DocBlocker

Easily add Doc Blocks to your methods.

Duplicate Action

Add the ability to duplicate selected folders and files.

Git History

Git History

With Git History you can visually see changes to files, perfect when you need to restore to an earlier commit, and want to ensure you have the right commit.

Go to method

Go to method

The standard Go to Symbol in File... feature of VS Code can be noisy when you're only interested in functions. This extension adds a Go to Method in File... feature that allows you to focus only on the functions declared in the file.

Highlight Matching Tag

Highlight Matching Tag

Visually see the starting and ending tags for a selected element, coming in handy when inside nested elements.

HTML Boilerplate

HTML Boilerplate

Handy for starting a new HMTL page, generate the starter code.

HTML Snippets

HTML Snippets

Easily autocomplete HTML snippets

Laravel Artisan

Laravel Artisan

Run Laravel Artisan commands I love the route:list call. A time saver for larger projects.

Laravel Blade

Add Blade Syntax highlighting

Markdown Preview Enhanced

Markdown Preview Enhanced

Prefix Markdown as you write it.

PHP Intelephense

PHP code intelligence for Visual Studio Code.

Intelephense is a high-performance PHP language server packed full of essential features for productive PHP development.

PHP IntelliSense

PHP Intellisense

Advanced PHP IntelliSense for Visual Studio Code.

Polacode

Polacode

Easy turn code into a shareable image.

Project Manager

Easily set up and switch between projects.

Remote SSH

Remote SSH

Work on remote project over SSH

Sync Settings

Sync Settings

Sync all your settings, keyboard shortcuts and extensions to Github to easily sync VS Code on other machines or as a backup!

SFTP

Very powerful, with smart features. Very simple, requires just three lines of config

Snippet Creator

This extension helps to automate snippet creation. Select the code you want to create snippet from and use command Create Snippet from the command palette or your custom keybind.

Sublime Text Keymap and Settings Importer

Keymap

Moving from Sublime Text, then use this to importing all your settings and keymapping.

Wakatime

Waakatime

Wakatime is great for recording how long you spend writing code.

]]>
Mon, 01 Jun 2020 12:42:26 GMT https://dcblog.dev/my-favourite-vs-code-extensions https://dcblog.dev/my-favourite-vs-code-extensions
My Mac Development Tools

What I use for day to day software development, communication, and workflow.

Development software

Alfred App

Alfred is a great tool to quickly search your Mac for a program/file or run calculations on the fly or fire off a Google search. I even use it to search the PHP manual. With the powerpack addon, you can save your clipboard history which I use constantly.

VSCode

Is my code editor of choice, great for book writing and coding.

Table Plus

Using a database client is essential, Table Plus is great for managing multiple databases and inspecting the data.

iTerm

One of the first things I install on a new Mac it's a great terminal application has support for keyboard shortcuts and themes.

Browser

Chrome

Chrome is my preferred editor for day to day use and for it's extensions for development.

Software Packages

Setapp

Set app is a subscription service for Mac, it has loads of great apps to use such as Table Plus, Cloud Mounter, Clean My Mac, and many more.

Communications

Microsoft Outlook

Outlook is my go-to Email client, nothing else comes close to it. For exchange emails and calendars.

Microsoft Teams

I keep in touch with the team using Teams, also great for sharing resources and video chats with clients.

Music

Spotify

Spotify keeps me focused whilst coding, the best music streaming service in my opinion.

Notes

Bearapp

I love bear it's the best note-taking app or Mac.

]]>
Sun, 31 May 2020 14:42:06 GMT https://dcblog.dev/my-mac-development-tools https://dcblog.dev/my-mac-development-tools
Converting MSSQL to MySQL

Recently I've inherited a project build in MSSQL that needs to be converting into MySQL. For anyone who has looked into this you'll quickly realise its not a simple conversion. There are a few tools you can try and also a manual way.

First the manual way:

This involved writing a script that will read from a MSSQL database and write to a MySQL database, all these scripts are in the same folder:

For the database calls I modified my PDO Wrapper, this version can be accessed at https://github.com/dcblogdev/pdo-wrapper/blob/mssql/src/Database.php

I then include db.php into a config.php file. Set up credentials for the MSSQL that I call $old and the credentials for the MySQL database that I call $new

<?php
require 'db.php';

$old_username = 'username';
$old_password = 'password';
$old_database = 'databasse'; 
$old_host = 'ip address';
$old_type = 'mssql';

$new_username = 'username';
$new_password = 'password';
$new_database = 'databasse'; 
$new_host = 'ip address';
$new_type = 'mysql';

$old = Database::get($old_username, $old_password, $old_database, $old_host, $old_type);
$new = Database::get($new_username, $new_password, $new_database, $new_host, $new_type);

I then include config.php into any file I want to run the conversion on.

I truncate the new table so I can run the script as needed. Next I select all records from a table using the $old connection, loop over the data and insert into the MySQL database using the $new connection.

<?php
require 'config.php';

$new->truncate('car_parking');

$rows = $old->select("SELECT * from dbo.CarParking");
foreach ($rows as $row) {

    $data = [
        'id'  => $row->CarParkingID,
        'title'   => $row->Title,
    ];

    $new->insert('car_parking', $data);
}

At any time I can run this in a terminal/command prompt by typing php followed by the filename ie php carparking.php

This will work but if you are working with a large database it will take a long time to write the queries for all tables.

Tools

MySQL Workbench

You can use MySQL Workbench to run a migration wizard. I found it fairly complicated to setup and slow going and worse some tables that were see as the wrong format were missed entirly.

MS SQL to MySQL converter

The MSSQL-to-MySQL converter works great, it's easy to setup assuming both databases are on the same machine. You can convert directly into a MySQL database or to a SQL file. Either specifing specific tables or all tables. The trail version is limited to 50 records for a complete conversion you will need to buy the product. At $49 is a bargain will save hours of work, I highly recommend this approach.

]]>
Sat, 30 May 2020 23:33:07 GMT https://dcblog.dev/converting-mssql-to-mysql https://dcblog.dev/converting-mssql-to-mysql
Composer Allowed memory size error

Running composer to install a new package locally caused the following error:

PHP Fatal error: Allowed memory size of 1610612736 bytes exhausted (tried to allocate 4096 bytes) in phar:///usr/local/bin/composer/src/Composer/DependencyResolver/Solver.php on line 223

The solution was to allow my local setup to consume as much memory as it needs to run:

php -d memory_limit=-1 /usr/local/bin/composer require --dev knuckleswtf/scribe
]]>
Wed, 27 May 2020 08:41:50 GMT https://dcblog.dev/composer-allowed-memory-size-error https://dcblog.dev/composer-allowed-memory-size-error
composer install killed

If you eve run composer install and terminal hands and then shows killed it means the server does not have enough memory to install the packages, this is common if you do not have a composer.lock file in the repository.

IF you do not have a composer.lock file in your respositoy then should:

ensure compoer.lock is not in a .gitignore file Run composer install in a local environment (either your local physical machine, or a development virtual machine)

With the updated composer.lock file commit the file and push it to your repository.

Then on the server git pull and then you should be able to do a composer install using the same versions as you've done locally.

]]>
Fri, 22 May 2020 07:38:26 GMT https://dcblog.dev/composer-install-killed https://dcblog.dev/composer-install-killed
Reversing and Re-ordering ol items in HTML

I've just learned you can alter HTML ol items. You can reserver the order and change the starting number.

Thanks to @IMAC2 for this tip.

To reverse the order add <ol reversed>

To change the starting number add a start <ol start='15'> which tells HTML to start the number at 15.

]]>
Wed, 20 May 2020 08:46:31 GMT https://dcblog.dev/reversing-and-re-ordering-ol-items-in-html https://dcblog.dev/reversing-and-re-ordering-ol-items-in-html
MySQL 8 The server requested authentication method unknown to the client

On a new installation of MySQL 8 you may come across the error below when running application.

The server requested authentication method unknown to the client

The simple fix is to go login to the MySQL shell:

mysql -uroot

then run:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';

This updates the root user to be identified with no password (or the one you've specified).

]]>
Wed, 13 May 2020 23:50:02 GMT https://dcblog.dev/mysql-8-the-server-requested-authentication-method-unknown-to-the-client https://dcblog.dev/mysql-8-the-server-requested-authentication-method-unknown-to-the-client
Software/tools for self-publishing books

I've recently published 2 books with Packt publishing which was an interesting experience but now I'm looking to start self-publishing.

My question is what software would you recommend/use for authoring books? I used Microsoft Word with Packt. Now I'm looking the following software looks promising:

  • Ulysses App
  • Bear App
  • Google Docs
  • Scrivener

I'm thinking something web-based may be best then I can write on any device.

Each of these has epub export formats not sure whether I should care about the export options before I have even started writing?

Ideally, I'd like a tool that can create a table of contents that links to the chapters, can export to epub, and can help in making a manuscript file.

Would love to hear what other people have used.

]]>
Thu, 07 May 2020 12:39:34 GMT https://dcblog.dev/softwaretools-for-self-publishing-books https://dcblog.dev/softwaretools-for-self-publishing-books
jQuery Fullcalender with PHP and MySQL

I'm a big fan of fullcalendar.io the latest version is quite a bit different from earlier versions, in this tutorial I'll cover how to integrate fullcalendar into a database with MySQL and load the events from PHP.

 

I've downloaded files from https://fullcalendar.io/docs/getting-started also I've added the following:

Bootstrap v4.4.1
jQuery v3.2.1
jQuery UI v1.10.3
Datepicker v1.6.3
Colorpicker

I've placed all the downloaded packages and the ones listed above in a folder called packages, Also I've got a folder called api where all the database files will live.

I'm going to use a PDO Wrapper for the database interactions to install it create a file called composer.json and add:

{
    "require": {
        "dcblogdev/pdo-wrapper": "^1.1"
    }
}

Now run `composer install` in a terminal to install the wrapper. A vendor folder will be created.

Now create a config.php file and add:

<?php
require('vendor/autoload.php');

use Dcblogdev\PdoWrapper\Database;

$host = "localhost";
$database = "calendar";
$username = "root";
$password = "";

$db = Database::get($username, $password, $database, $host);
$dir = "./";

First, we include the autoloader so any files from composer can be loaded.

Make an instance of the Database wrapper and set the database credentials.

Finally set the folder path $dir = "./" points to the current folder, you can make this absolute if you prefer.

Create a table called events with the following structure:

CREATE TABLE `events` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` text,
  `start_event` datetime NOT NULL,
  `end_event` datetime NOT NULL,
  `color` varchar(191) DEFAULT NULL,
  `text_color` varchar(191) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

You can import the db.sql file provided on my GitHub repo or run this in your MySQL program of choice.

Create a file called index.php this is where the calender will be used.

Include the config and the packages:

<?php require('config.php');?>
<!DOCTYPE html>
<html>
<head>
    <title>Calandar</title>
    
    <link href='<?=$dir;?>packages/core/main.css' rel='stylesheet' />
    <link href='<?=$dir;?>packages/daygrid/main.css' rel='stylesheet' />
    <link href='<?=$dir;?>packages/timegrid/main.css' rel='stylesheet' />
    <link href='<?=$dir;?>packages/list/main.css' rel='stylesheet' />
    <link href='<?=$dir;?>packages/bootstrap/css/bootstrap.css' rel='stylesheet' />
    <link href="<?=$dir;?>packages/jqueryui/custom-theme/jquery-ui-1.10.4.custom.min.css" rel="stylesheet">
    <link href='<?=$dir;?>packages/datepicker/datepicker.css' rel='stylesheet' />
    <link href='<?=$dir;?>packages/colorpicker/bootstrap-colorpicker.min.css' rel='stylesheet' />
    <link href='<?=$dir;?>style.css' rel='stylesheet' />

    <script src='<?=$dir;?>packages/core/main.js'></script>
    <script src='<?=$dir;?>packages/daygrid/main.js'></script>
    <script src='<?=$dir;?>packages/timegrid/main.js'></script>
    <script src='<?=$dir;?>packages/list/main.js'></script>
    <script src='<?=$dir;?>packages/interaction/main.js'></script>
    <script src='<?=$dir;?>packages/jquery/jquery.js'></script>
    <script src='<?=$dir;?>packages/jqueryui/jqueryui.min.js'></script>
    <script src='<?=$dir;?>packages/bootstrap/js/bootstrap.js'></script>
    <script src='<?=$dir;?>packages/datepicker/datepicker.js'></script>
    <script src='<?=$dir;?>packages/colorpicker/bootstrap-colorpicker.min.js'></script>
    <script src='<?=$dir;?>calendar.js'></script>
</head>
<body>

The calendar.js is where all the calendar functions will go.

Now add the calender markup:

<div class="container">

    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addeventmodal">
      Add Event
    </button>

    <div id="calendar"></div>
</div>

</body>
</html>

By default, the calendar will be attached to an element by its id.

Also since we're using bootstrap we can make use of its modals to load popup windows for adding and editing events.

the markup for these:

<div class="modal fade" id="addeventmodal" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">

            <div class="modal-header">
                <h5 class="modal-title">Add Event</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

            <div class="modal-body">

                <div class="container-fluid">

                    <form id="createEvent" class="form-horizontal">

                    <div class="row">

                        <div class="col-md-6">

                            <div id="title-group" class="form-group">
                                <label class="control-label" for="title">Title</label>
                                <input type="text" class="form-control" name="title">
                                <!-- errors will go here -->
                            </div>

                            <div id="startdate-group" class="form-group">
                                <label class="control-label" for="startDate">Start Date</label>
                                <input type="text" class="form-control datetimepicker" id="startDate" name="startDate">
                                <!-- errors will go here -->
                            </div>

                            <div id="enddate-group" class="form-group">
                                <label class="control-label" for="endDate">End Date</label>
                                <input type="text" class="form-control datetimepicker" id="endDate" name="endDate">
                                <!-- errors will go here -->
                            </div>

                        </div>

                        <div class="col-md-6">

                            <div id="color-group" class="form-group">
                                <label class="control-label" for="color">Colour</label>
                                <input type="text" class="form-control colorpicker" name="color" value="#6453e9">
                                <!-- errors will go here -->
                            </div>

                            <div id="textcolor-group" class="form-group">
                                <label class="control-label" for="textcolor">Text Colour</label>
                                <input type="text" class="form-control colorpicker" name="text_color" value="#ffffff">
                                <!-- errors will go here -->
                            </div>

                        </div>

                    </div>

                    

                </div>

            </div>

            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
              <button type="submit" class="btn btn-primary">Save changes</button>
            </div>

            </form>

        </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

<div class="modal fade" id="editeventmodal" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">

            <div class="modal-header">
                <h5 class="modal-title">Update Event</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

            <div class="modal-body">

                <div class="container-fluid">

                    <form id="editEvent" class="form-horizontal">
                    <input type="hidden" id="editEventId" name="editEventId" value="">

                    <div class="row">

                        <div class="col-md-6">

                            <div id="edit-title-group" class="form-group">
                                <label class="control-label" for="editEventTitle">Title</label>
                                <input type="text" class="form-control" id="editEventTitle" name="editEventTitle">
                                <!-- errors will go here -->
                            </div>

                            <div id="edit-startdate-group" class="form-group">
                                <label class="control-label" for="editStartDate">Start Date</label>
                                <input type="text" class="form-control datetimepicker" id="editStartDate" name="editStartDate">
                                <!-- errors will go here -->
                            </div>

                            <div id="edit-enddate-group" class="form-group">
                                <label class="control-label" for="editEndDate">End Date</label>
                                <input type="text" class="form-control datetimepicker" id="editEndDate" name="editEndDate">
                                <!-- errors will go here -->
                            </div>

                        </div>

                        <div class="col-md-6">

                            <div id="edit-color-group" class="form-group">
                                <label class="control-label" for="editColor">Colour</label>
                                <input type="text" class="form-control colorpicker" id="editColor" name="editColor" value="#6453e9">
                                <!-- errors will go here -->
                            </div>

                            <div id="edit-textcolor-group" class="form-group">
                                <label class="control-label" for="editTextColor">Text Colour</label>
                                <input type="text" class="form-control colorpicker" id="editTextColor" name="editTextColor" value="#ffffff">
                                <!-- errors will go here -->
                            </div>

                        </div>

                    </div>

                </div>

            </div>

            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
              <button type="submit" class="btn btn-primary">Save changes</button>
              <button type="button" class="btn btn-danger" id="deleteEvent" data-id>Delete</button>
            </div>

            </form>

        </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

These both have forms to add and edit the event.

Now lets move on to the JS create a file called calendar.js

add an event listener so the code will only run once the page is fully loaded:

document.addEventListener('DOMContentLoaded', function() {

    var url ='/';

Now we will setup a class for the datepicker to attach to

$('body').on('click', '.datetimepicker', function() {
    $(this).not('.hasDateTimePicker').datetimepicker({
        controlType: 'select',
        changeMonth: true,
        changeYear: true,
        dateFormat: "dd-mm-yy",
        timeFormat: 'HH:mm:ss',
        yearRange: "1900:+10",
        showOn:'focus',
        firstDay: 1
    }).focus();
});

And the color picker

$(".colorpicker").colorpicker();

Now setup a variable for the calendar to attach to a div with an id of calendar

var calendarEl = document.getElementById('calendar');

Now for the FullCalendar 

var calendar = new FullCalendar.Calendar(calendarEl, {

Inside FullCalendar place the calendar options:

plugins: ['interaction', 'dayGrid', 'timeGrid', 'list'],
header: {
    left: 'prev,next today',
    center: 'title',
    right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
},
navLinks: true, // can click day/week names to navigate views
businessHours: true, // display business hours
editable: true,

to load the events we need to call events:

events: url+'api/load.php',

This sends a request to load.php which will return a JSON array of all the events, I'll cover the JS functions first then go over the PHP code.

Now can call 3 additional options, eventDrop, eventResize and eventClick

eventDrop is triggered when an event is dragged and dropped to another date or time.

We collect the start and end dates and then send an ajax request to update.php and pass the id of the event and the start and end time.

eventDrop: function(arg) {
    var start = arg.event.start.toDateString()+' '+arg.event.start.getHours()+':'+arg.event.start.getMinutes()+':'+arg.event.start.getSeconds();
    if (arg.event.end == null) {
        end = start;
    } else {
        var end = arg.event.end.toDateString()+' '+arg.event.end.getHours()+':'+arg.event.end.getMinutes()+':'+arg.event.end.getSeconds();
    }

    $.ajax({
      url:url+"api/update.php",
      type:"POST",
      data:{id:arg.event.id, start:start, end:end},
    });
}

eventReize is very similar, this is triggered when an event is manually resized from a week or day view.

eventResize: function(arg) {
    var start = arg.event.start.toDateString()+' '+arg.event.start.getHours()+':'+arg.event.start.getMinutes()+':'+arg.event.start.getSeconds();
    var end = arg.event.end.toDateString()+' '+arg.event.end.getHours()+':'+arg.event.end.getMinutes()+':'+arg.event.end.getSeconds();

    $.ajax({
      url:url+"api/update.php",
      type:"POST",
      data:{id:arg.event.id, start:start, end:end},
    });
}

eventClick is triggered when an event is clicked on, at this point we want to collect the id of the event and then do an ajax call to get all the event details, add them to a model and show the modal. Also check if a delete button has been pressed on the modal and if so send an ajax event to delete the entry.

eventClick: function(arg) {
    var id = arg.event.id;
    
    $('#editEventId').val(id);
    $('#deleteEvent').attr('data-id', id); 

    $.ajax({
      url:url+"api/getevent.php",
      type:"POST",
      dataType: 'json',
      data:{id:id},
      success: function(data) {
            $('#editEventTitle').val(data.title);
            $('#editStartDate').val(data.start);
            $('#editEndDate').val(data.end);
            $('#editColor').val(data.color);
            $('#editTextColor').val(data.textColor);
            $('#editeventmodal').modal();
        }
    });

    $('body').on('click', '#deleteEvent', function() {
        if(confirm("Are you sure you want to remove it?")) {
            $.ajax({
                url:url+"api/delete.php",
                type:"POST",
                data:{id:arg.event.id},
            }); 

            //close model
            $('#editeventmodal').modal('hide');

            //refresh calendar
            calendar.refetchEvents();         
        }
    });
    
    calendar.refetchEvents();
}

On the model for adding an event we need to send the form data to PHP and process the response, we do this by sending an ajax call:

$('#createEvent').submit(function(event) {

    // stop the form refreshing the page
    event.preventDefault();

    $('.form-group').removeClass('has-error'); // remove the error class
    $('.help-block').remove(); // remove the error text

    // process the form
    $.ajax({
        type        : "POST",
        url         : url+'api/insert.php',
        data        : $(this).serialize(),
        dataType    : 'json',
        encode      : true
    }).done(function(data) {

        // insert worked
        if (data.success) {
            
            //remove any form data
            $('#createEvent').trigger("reset");

            //close model
            $('#addeventmodal').modal('hide');

            //refresh calendar
            calendar.refetchEvents();

        } else {

            //if error exists update html
            if (data.errors.date) {
                $('#date-group').addClass('has-error');
                $('#date-group').append('<div class="help-block">' + data.errors.date + '</div>');
            }

            if (data.errors.title) {
                $('#title-group').addClass('has-error');
                $('#title-group').append('<div class="help-block">' + data.errors.title + '</div>');
            }

        }

    });
});

The process for editing an event is similar:

$('#editEvent').submit(function(event) {

    // stop the form refreshing the page
    event.preventDefault();

    $('.form-group').removeClass('has-error'); // remove the error class
    $('.help-block').remove(); // remove the error text

    //form data
    var id = $('#editEventId').val();
    var title = $('#editEventTitle').val();
    var start = $('#editStartDate').val();
    var end = $('#editEndDate').val();
    var color = $('#editColor').val();
    var textColor = $('#editTextColor').val();

    // process the form
    $.ajax({
        type        : "POST",
        url         : url+'api/update.php',
        data        : {
            id:id, 
            title:title, 
            start:start,
            end:end,
            color:color,
            text_color:textColor
        },
        dataType    : 'json',
        encode      : true
    }).done(function(data) {

        // insert worked
        if (data.success) {
            
            //remove any form data
            $('#editEvent').trigger("reset");

            //close model
            $('#editeventmodal').modal('hide');

            //refresh calendar
            calendar.refetchEvents();

        } else {

            //if error exists update html
            if (data.errors.date) {
                $('#date-group').addClass('has-error');
                $('#date-group').append('<div class="help-block">' + data.errors.date + '</div>');
            }

            if (data.errors.title) {
                $('#title-group').addClass('has-error');
                $('#title-group').append('<div class="help-block">' + data.errors.title + '</div>');
            }

        }

    });
});

At this point we have the calender and all the ajax calls next we need PHP to process the requests.

Create a folder called api and the following files:

delete.php
getevent.php
insert.php
load.php
update.php

delete.php

Check if an id exists in the POST request and if do then attempt to delete the event where the id posted matched an id from the events table.

include("../config.php");

if (isset($_POST["id"])) {
    $db->delete('events', ['id' => $_POST['id']]);
}

getevent

Check if an id exists in the POST request then load the event that matched the id and then create a data array matching the structure the calendar needs and finally return the array as JSON.

<?php
include("../config.php");

if (isset($_POST['id'])) {
    $row = $db->find("* FROM events where id=?", [$_POST['id']]);
    $data = [
        'id'        => $row->id,
        'title'     => $row->title,
        'start'     => date('d-m-Y H:i:s', strtotime($row->start_event)),
        'end'       => date('d-m-Y H:i:s', strtotime($row->end_event)),
        'color'     => $row->color,
        'textColor' => $row->text_color
    ];

    echo json_encode($data);
}

insert.php

here we validate the data meets our requirements by ensuring the title, start and end are not empty then format the data before inserting the data. We always return JSON data to the user can be show validation errors or to close the model on success.

<?php
include("../config.php");

if (isset($_POST['title'])) {

    //collect data
    $error      = null;
    $title      = $_POST['title'];
    $start      = $_POST['startDate'];
    $end        = $_POST['startDate'];
    $color      = $_POST['color'];
    $text_color = $_POST['text_color'];

    //validation
    if ($title == '') {
        $error['title'] = 'Title is required';
    }

    if ($start == '') {
        $error['start'] = 'Start date is required';
    }

    if ($end == '') {
        $error['end'] = 'End date is required';
    }

    //if there are no errors, carry on
    if (! isset($error)) {

        //format date
        $start = date('Y-m-d H:i:s', strtotime($start));
        $end = date('Y-m-d H:i:s', strtotime($end));
        
        $data['success'] = true;
        $data['message'] = 'Success!';

        //store
        $insert = [
            'title'       => $title,
            'start_event' => $start,
            'end_event'   => $end,
            'color'       => $color,
            'text_color'  => $text_color
        ];
        $db->insert('events', $insert);
      
    } else {

        $data['success'] = false;
        $data['errors'] = $error;
    }

    echo json_encode($data);
}

load.php

this file loads all the events for the calendar to read

<?php
include("../config.php");
$data = [];

$result = $db->select("* FROM events ORDER BY id");
foreach($result as $row) {
    $data[] = [
        'id'              => $row->id,
        'title'           => $row->title,
        'start'           => $row->start_event,
        'end'             => $row->end_event,
        'backgroundColor' => $row->color,
        'textColor'       => $row->text_color
    ];
}

echo json_encode($data);

update.php

this again will validate all required fields have been entered and then perform an update 

<?php
include("../config.php");

if (isset($_POST['id'])) {

    //collect data
    $error      = null;
    $id         = $_POST['id'];
    $start      = $_POST['start'];
    $end        = $_POST['end'];

    //optional fields
    $title      = isset($_POST['title']) ? $_POST['title']: '';
    $color      = isset($_POST['color']) ? $_POST['color']: '';
    $text_color = isset($_POST['text_color']) ? $_POST['text_color']: '';

    //validation
    if ($start == '') {
        $error['start'] = 'Start date is required';
    }

    if ($end == '') {
        $error['end'] = 'End date is required';
    }

    //if there are no errors, carry on
    if (! isset($error)) {

        //reformat date
        $start = date('Y-m-d H:i:s', strtotime($start));
        $end = date('Y-m-d H:i:s', strtotime($end));
        
        $data['success'] = true;
        $data['message'] = 'Success!';

        //set core update array
        $update = [
            'start_event' => date('Y-m-d H:i:s', strtotime($_POST['start'])),
            'end_event' => date('Y-m-d H:i:s', strtotime($_POST['end']))
        ];

        //check for additional fields, and add to $update array if they exist
        if ($title !='') {
            $update['title'] = $title;
        }

        if ($color !='') {
            $update['color'] = $color;
        }

        if ($text_color !='') {
            $update['text_color'] = $text_color;
        }

        //set the where condition ie where id = 2
        $where = ['id' => $_POST['id']];

        //update database
        $db->update('events', $update, $where);
      
    } else {

        $data['success'] = false;
        $data['errors'] = $error;
    }

    echo json_encode($data);
}

At this point, events can be added, edit, delete and so dragged to other days. 

I'd recommend looking at the complete source code available on:

]]>
Tue, 14 Apr 2020 10:54:30 GMT https://dcblog.dev/jquery-fullcalender-with-php-and-mysql https://dcblog.dev/jquery-fullcalender-with-php-and-mysql
MySQL categories and subcategories

Working with categories is a common task, also is the need for using subcategories, in this tutorial I will explain how to design a database schema to support both categories and subcategories from a single table. Let's start with the schema:

CREATE TABLE `categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) NOT NULL DEFAULT '0',
  `category` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

In order to store categories and subcategories together, we need a way to determine what categories are the subcategories. This can be accomplished by the parent_id column, all root categories will have a parent_id of 0. The subcategories will have a parent_id that matches the id of the parent category.

Let's add some data to make this clearer.

INSERT INTO `categories` (`id`, `parent_id`, `category`) VALUES
(1, 0, 'General'),
(2, 0, 'PHP'),
(3, 0, 'HTML'),
(4, 3, 'Tables'),
(5, 2, 'Functions'),
(6, 2, 'Variables'),
(7, 3, 'Forms');

Here we have 3 categories (General, PHP & HTML) and 4 subcategories.

an easier way to read this:

From this image, we can see that Tables is a subcategory of HTML we can tell as it's parent_id is 3 and HTML has an id of 3.

Now we can see how categories and subcategories are stored let's put this into practice.

I'll use PDO for these examples:

Connect to database:

$host = "localhost";
$database = "categories";
$username = "root";
$password = "";

$db = new PDO("mysql:host=$host;dbname=$database", $username, $password);

//turn on exceptions
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

//set default fetch mode
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);

Now select all root categories:

$categories = $db->query('SELECT id, parent_id, category FROM categories WHERE parent_id = 0 ORDER BY category');

Now loop over the categories and print the title, next perform another query to get to the subcategories based on the parent_id and loop over them.

<ul>
<?php
foreach($categories->fetchAll() as $row) {

    echo "<li>$row->category</li>";

    //get child categories
    $children = $db->prepare('SELECT id, parent_id, category FROM categories WHERE parent_id = ? ORDER BY category');
    $children->execute([$row->id]);

    //determine if there are child items
    $hasChildren = $children->rowCount() > 0 ? true : false;

    if ($hasChildren) {
        echo "<ul>";
    }

    foreach($children->fetchAll() as $child) {
        echo "<li>$child->category</li>";
    }

    if ($hasChildren) {
        echo "</ul>";
    }
     
}
?>
</ul>

One issue this setup is if there are another level of subcategories they would not be displayed unless another loop is added.

Let's tackle this, select all categories regardless of the parent. Add all the results as an array and pass them to a function called generateTree.

$categories = $db->query('SELECT id, parent_id, category FROM categories ORDER BY category');
$rows = $categories->fetchAll(PDO::FETCH_ASSOC);
echo generateTree($rows);

This function will loop itself and show the title, and will recall the function for the depth needed until all loops have finished.

function generateTree($data, $parent = 0, $depth=0)
{
    $tree = "<ul>\n";
    for ($i=0, $ni=count($data); $i < $ni; $i++) {
        if ($data[$i]['parent_id'] == $parent) {    
            
            $tree .= "<li>\n";
            $tree .= $data[$i]['category'];
            $tree .= generateTree($data, $data[$i]['id'], $depth+1);
            $tree .= "</li>\n";
        }
    }
    $tree .= "</ul>\n";
    return $tree;
}

Which outputs the following based on this data:

INSERT INTO `categories` (`id`, `parent_id`, `category`) VALUES
(1, 0, 'General'),
(2, 0, 'PHP'),
(3, 0, 'HTML'),
(4, 3, 'Tables'),
(5, 2, 'Functions'),
(6, 2, 'Variables'),
(7, 3, 'Forms'),
(8, 5, 'sub 1'),
(9, 8, 'sub 2');
  • General
  • HTML
    • Forms
    • Tables
  • PHP
    • Functions
    • Variables
  • General
  • HTML
    • Forms
    • Tables
  • PHP
    • Functions
      • sub 1
        • sub 2
    • Variables
]]>
Sun, 12 Apr 2020 21:34:33 GMT https://dcblog.dev/mysql-categories-and-subcategories https://dcblog.dev/mysql-categories-and-subcategories
Writing to an existing PDF with FPDI

FPDI allows existing PDF's to be used as a template for a new PDF. Whilst this does not give the ability to edit a PDF it can import one and add to it.

The FPDI extends FPDF so a copy of the latest version will be needed to use, place it in the same directory or update paths to it as needed.

It's recommended to use composer to install the library, you can do it like this:

In a composer.json file add the dependencies:

{
    "require": {
        "setasign/fpdf": "1.8.*",
        "setasign/fpdi": "^2.0"
    }
}

Then do a composer install in a terminal this will generate a vendor folder where the libraries are installed into.

Here's an example with comments.

<?php
require_once('vendor/autoload.php');

use setasign\Fpdi\Fpdi;

// initiate FPDI
$pdf = new Fpdi();

// add a page
$pdf->AddPage();

// set the source file
$pdf->setSourceFile("demo.pdf");

// import page 1
$tplId = $pdf->importPage(1);

// use the imported page and place it at point 10,10 with a width of 100 mm
$pdf->useTemplate($tplId);

// The new content
$fontSize = '15';
$fontColor = `255,0,0`;
$left = 16;
$top = 40;
$text = 'Sample Text over overlay';

//set the font, colour and text to the page.
$pdf->SetFont("helvetica", "B", 15);
$pdf->SetTextColor($fontColor);
$pdf->Text($left,$top,$text);

//see the results
$pdf->Output();  

There are no built in options to get the page numbers so if you want to add them you need to know how many pages the PDF has then loop over them like this:

//optionally add additional pages, start the $i after the current page ie start from 2
for ($i=2; $i < 6; $i++) {
    $pdf->AddPage();
    $tplId = $pdf->importPage($i);
    $pdf->useTemplate($tplId);
}

 

]]>
Fri, 10 Apr 2020 19:00:35 GMT https://dcblog.dev/writing-to-an-existing-pdf-with-fpdi https://dcblog.dev/writing-to-an-existing-pdf-with-fpdi
Easily assign variable when using explode in PHP

I use explode all the time, for splitting strings into parts. I typically write my explodes like this:

//set the string
$name = 'Joe Bloggs';

//explode where there is a space
$parts = explode(' ', $name);

//add the parts to variables
$firstName = $parts[0];
$lastName = $parts[1];

But I've recently found out you can assign variables during the explode. Really handy!

This is how do you do it. Using an array to specify the name variables then assign them a value from explode:

//set the string
$name = 'Joe Bloggs';

//explode where there is a space and assign the indexes to named variables
[$firstName, $lastName] = explode(' ', $name);

A must cleaner way in my opinion.

]]>
Fri, 10 Apr 2020 16:04:34 GMT https://dcblog.dev/easily-assign-variable-when-using-explode-in-php https://dcblog.dev/easily-assign-variable-when-using-explode-in-php
Latitude and Longitude with Google Maps

Google Maps allows using its service to get map coordinates:

For instance, In your HTML, two fields to store the latitude and longitude 

<input id="latitude" type="text" name="latitude" value="">
<input id="longitude" type="text" name="longitude" value="">

When a postcode is entered use Google Maps to update the latitude and longitude:

<input id="postcode" type="text" name="postcode" value="">

The JS code, note the <?=$apiKey;?> should contain your Google Maps API key.

<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<?=$apiKey;?>"></script>
<script type="text/javascript">
    $(document).on('change', '#postcode', function (e) {
        var geocoder = new google.maps.Geocoder();
        var address = this.value;
        geocoder.geocode({ 'address': address }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                var latitude = results[0].geometry.location.lat();
                var longitude = results[0].geometry.location.lng();

                //set the value of input elements with an id of latitude and longitude
                document.getElementById("latitude").value = latitude;
                document.getElementById("longitude").value = longitude;
            } 
        });
    });
</script>

 

]]>
Thu, 12 Mar 2020 18:33:42 GMT https://dcblog.dev/latitude-and-longitude-with-google-maps https://dcblog.dev/latitude-and-longitude-with-google-maps
Laravel Merge PDFs

Ever needed to merge multiple PDF's together? It's a common need. In this tutorial, I'll cover how to do merge multiple pdf's together in Laravel.

First, we need a package called lara-pdf-merger Install it with composer:

composer require daltcore/lara-pdf-merger

Next, create import the namespace:

use LynX39\LaraPdfMerger\Facades\PdfMerger;

Create a new instance:

$pdfMerger = PDFMerger::init();

Add PDF to be merged:

$pdfMerger->addPDF(base_path('Modules/Quotes/pages/1.pdf'), 'all');

Do this for each PDF to be merged.

$pdfMerger->addPDF(base_path('Modules/Quotes/pages/1.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/2.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/3.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/4.pdf'), 'all');

Next, call a merge method to perform the merge.

$pdfMerger->merge();

Next, save the PDF to disk

$pdfMerger->save(public_path('quotes/001.pdf'), "file");

Putting this all together:

$pdfMerger = PDFMerger::init(); //Initialize the merger
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/1.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/2.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/3.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/4.pdf'), 'all');
$pdfMerger->merge();
$pdfMerger->save(public_path('quotes/001.pdf'), "file");

Generate a PDF and merge with existing

You may want to generate new PDF and merge that in with existing PDFs.

Let's install Laravel DOMPDF to make a new PDF. Install with composer:

composer require barryvdh/laravel-dompdf

Import PDF:

use PDF;

Create a new PDF, load a view file (quotes/pdf.blade.php) for the contents of the PDF and save to a quotes folder and use the filename quote-001.pdf

$filename = "quote-001.pdf";
$pdf = PDF::loadView('quotes.pdf', ['quote' => $data]);
$pdf->save('quotes/'.$filename);

Now, this can be merged using the same technique above.

$filename = "quote-001.pdf";
$pdf = PDF::loadView('quotes/pdf', ['quote' => $record]);
$pdf->save('quotes/'.$filename);

$pdfMerger = PDFMerger::init(); //Initialize the merger
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/1.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/2.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/3.pdf'), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/4.pdf'), 'all');
$pdfMerger->addPDF(public_path('quotes/'.$filename), 'all');
$pdfMerger->addPDF(base_path('Modules/Quotes/pages/5.pdf'), 'all');
$pdfMerger->merge();
$pdfMerger->save(public_path('quotes/quote-001.pdf'), "file");

 

]]>
Mon, 02 Mar 2020 07:14:02 GMT https://dcblog.dev/laravel-merge-pdfs https://dcblog.dev/laravel-merge-pdfs
Laravel API change unauthenticated message

When making API calls to Laravel when a user who is not authenticated makes a call a 401 status code is returned and the following response:

{"message":"Unauthenticated."}

Which I find a little strange it's an error so message should say error in my opinion.

This can easily be changed by adding a customer unauthenticated method to app/Exceptions/Handler.php:

protected function unauthenticated($request, AuthenticationException $exception) 
{
    if ($request->expectsJson()) {
        return response()->json(['error' => 'Unauthenticated.'], 401);
    }

    return redirect()->guest('login');
}

Now the response will say error instead of message.

]]>
Sat, 29 Feb 2020 21:24:27 GMT https://dcblog.dev/laravel-api-change-unauthenticated-message https://dcblog.dev/laravel-api-change-unauthenticated-message
Laravel returns 302 for unauthenticated calls

I've started to secure API calls with Laravel Airlock, when calling a route that's been secured with the middleware auth:airlock unauthenticated calls we're returning a status code of 302 means the route has been found. When 401 unauthenticated should have been returned.

Turns out it was not a Laravel Airlock issue but how Laravel handles ajax calls in order for an API call to be treated as an API call it requires a header called Accept and a value of application/json. With this heading in place, 401 will be returned for unauthenticated calls.

]]>
Sat, 29 Feb 2020 21:15:09 GMT https://dcblog.dev/laravel-returns-302-for-unauthenticated-calls https://dcblog.dev/laravel-returns-302-for-unauthenticated-calls