PHP & Web Development Blogs

Search Results For: push
Showing 1 to 5 of 6 blog articles.
25250 views · 4 years ago
Introduction to Gitlab CI for PHP developers
As a developer, you've probably at least heard something about CI - Continuous integration. And if you haven't - you better fix it ASAP, because that's something awesome to have on your skill list and can get extremely helpful in your everyday work. This post will focus on CI for PHP devs, and specifically, on CI implementation from Gitlab. I will suppose you know the basics of Git, PHP, PHPUnit, Docker and unix shell. Intended audience - intermediate PHP devs.
Adding something to your workflow must serve a purpose. In this case the goal is to automate routine tasks and achieve better quality control. Even a basic PHP project IMO needs the following:
* linter) checks (cannot merge changes that are invalid on the syntax level)
* Code style checks
* Unit and integration tests
All of those can be just run eventually, of course. But I prefer an automated CI approach even in my personal projects because it leads to a higher level of discipline, you simply can't avoid following a set of rules that you've developed. Also, it reduces a risk of releasing a bug or regression, thus improving quality.
Gitlab is as generous as giving you their CI for free, even for your private repos. At this point it is starting to look as advertising, therefore a quick comparison table for Gitlab, Github, Bitbucket. AFAIK, Github does not have a built-in solution, instead it is easily integrated with third parties, of which Travis CI seems to be the most popular - I will therefore mention Travis here.

Public repositories (OSS projects). All 3 providers have a free offer for the open-source community!


| Provider | Limits |
|---|---|
| Gitlab | 2,000 CI pipeline minutes per group per month, shared runners |
| Travis | Apparently unlimited |
| Bitbucket| 50 min/month, max 5 users, File storage <= 1Gb/month |

Private repositories


| Provider | Price | Limits |
|---|---|---|
| Gitlab | Free | 2,000 CI pipeline minutes per group per month, shared runners |
| Travis | $69/month | Unlimited builds, 1 job at a time |
| Bitbucket| Free | 50 min/month, max 5 users, File storage <= 1Gb/month |

Getting started

I made a small project based on Laravel framework and called it "ci-showcase". I work in Linux environment, and the commands I use in the examples, are for linux shell. They should be pretty much the same on Mac and nearly the same on Windows though.
composer create-project laravel/laravel ci-showcase

Next, I went to gitlab website and created a new public project: https://gitlab.com/crocodile2u/ci-showcase. Cloned the repo and copied all files and folders from the newly created project - the the new git repo. In the root folder, I placed a .gitignore file:
.idea
vendor
.env

Then the .env file:
APP_ENV=development

Then I generated the application encryption key: php artisan key:generate, and then I wanted to verify that the primary setup works as expected: ./vendor/bin/phpunit, which produced the output OK (2 tests, 2 assertions). Nice, time to commit this: git commit &amp;&amp; git push

At this point, we don't yet have any CI, let's do something about it!

Adding .gitlab-ci.yml

Everyone going to implement CI with Gitlab, is strongly encouraged to bookmark this page: https://docs.gitlab.com/ee/ci/README.html. I will simply provide a short introduction course here plus a bit of boilerplate code to get you started easier.
First QA check that we're going to add is PHP syntax check. PHP has a built-in linter, which you can invoke like this: php -l my-file.php. This is what we're going to use. Because the php -l command doesn't support multiple files as arguments, I've written a small wrapper shell script and saved it to ci/linter.sh:
#!/bin/sh
files=<code>sh ci/get-changed-php-files.sh | xargs</code>last_status=0
status=0
# Loop through changed PHP files and run php -l on each
for f in "$files" ; do message=<code>php -l $f</code> last_status="$?" if [ "$last_status" -ne "0" ]; then # Anything fails -> the whole thing fails echo "PHP Linter is not happy about $f: $message" status="$last_status" fi
done
if [ "$status" -ne "0" ]; then echo "PHP syntax validation failed!"
fi
exit $status

Most of the time, you don't actually want to check each and every PHP file that you have. Instead, it's better to check only those files that have been changed. The Gitlab pipeline runs on every push to the repository, and there is a way to know which PHP files have been changed. Here's a simple script, meet ci/get-changed-php-files.sh:
#!/bin/sh
# What's happening here?
#
# 1. We get names and statuses of files that differ in current branch from their state in origin/master.
# These come in form (multiline)
# 2. The output from git diff is filtered by unix grep utility, we only need files with names ending in .php
# 3. One more filter: filter *out* (grep -v) all lines starting with R or D.
# D means "deleted", R means "renamed"
# 4. The filtered status-name list is passed on to awk command, which is instructed to take only the 2nd part
# of every line, thus just the filename
git diff --name-status origin/master | grep '\.php$' | grep -v "^[RD]" | awk '{ print }'

These scripts can easily be tested in your local environment ( at least if you have a Linux machine, that is ;-) ).
Now, as we have our first check, we'll finally create our .gitlab-ci.yml. This is where your pipeline is declared using YAML notation:
# we're using this beautiful tool for our pipeline: https://github.com/jakzal/phpqa
image: jakzal/phpqa:alpine
# For this sample pipeline, we'll only have 1 stage, in real-world you would like to also add at least "deploy"
stages: - QA
linter:
stage: QA
# this is the main part: what is actually executed
script: - sh ci/get-changed-php-files.sh | xargs sh ci/linter.sh

The first line is image: jakzal/phpqa:alpine and it's telling Gitlab that we want to run our pipeline using a PHP-QA utility by jakzal. It is a docker image containing PHP and a huge variety of QA-tools. We declare one stage - QA, and this stage by now has just a single job named linter. Every job can have it's own docker image, but we don't need that for the purpose of this tutorial. Our project reaches Step 2. Once I had pushed these changes, I immediately went to the project's CI/CD page. Aaaand.... the pipeline was already running! I clicked on the linter job and saw the following happy green output:
Running with gitlab-runner 11.9.0-rc2 (227934c0) on docker-auto-scale ed2dce3a
Using Docker executor with image jakzal/phpqa:alpine ...
Pulling docker image jakzal/phpqa:alpine ...
Using docker image sha256:12bab06185e59387a4bf9f6054e0de9e0d5394ef6400718332c272be8956218f for jakzal/phpqa:alpine ...
Running on runner-ed2dce3a-project-11318734-concurrent-0 via runner-ed2dce3a-srm-1552606379-07370f92...
Initialized empty Git repository in /builds/crocodile2u/ci-showcase/.git/
Fetching changes...
Created fresh repository.
From https://gitlab.com/crocodile2u/ci-showcase * [new branch] master -> origin/master * [new branch] step-1 -> origin/step-1 * [new branch] step-2 -> origin/step-2
Checking out 1651a4e3 as step-2...
Skipping Git submodules setup
$ sh ci/get-changed-php-files.sh | xargs sh ci/linter.sh
Job succeeded

It means that our pipeline was successfully created and run!

PHP Code Sniffer.

PHP Code Sniffer is a tool for keeping app of your PHP files in one uniform code style. It has a hell of customizations and settings, but here we will only perform simple check for compatibilty with PSR-2 standard. A good practice is to create a configuration XML file in your project. I will put it in the root folder. Code sniffer can use a few file names, of which I prefer phpcs.xml:
<?xml version="1.0"?>
/resources

I also will append another section to .gitlab-ci.yml:
code-style:	stage: QA	script:	# Variable $files will contain the list of PHP files that have changes	- files=<code>sh ci/get-changed-php-files.sh</code> # If this list is not empty, we execute the phpcs command on all of them - if [ ! -z "$files" ]; then echo $files | xargs phpcs; fi

Again, we check only those PHP files that differ from master branch, and pass their names to phpcs utility. That's it, Step 3 is finished! If you go to see the pipeline now, you will notice that linter and code-style jobs run in parallel.

Adding PHPUnit

Unit and integration tests are essential for a successful and maintaiable modern software project. In PHP world, PHPUnit is de facto standard for these purposes. The PHPQA docker image already has PHPUnit, but that's not enough. Our project is based on Laravel, which means it depends on a bunch of third-party libraries, Laravel itself being one of them. Those are installed into vendor folder with composer. You might have noticed that our .gitignore file has vendor folder as one of it entries, which means that it is not managed by the Version Control System. Some prefer their dependencies to be part of their Git repository, I prefer to have only the composer.json declarations in Git. Makes the repo much much smaller than the other way round, also makes it easy to avoid bloating your production builds with libraries only needed for development.
Composer is also included into PHPQA docker image, and we can enrich our .gitlab-ci.yml:
test:	stage: QA	cache:	key: dependencies-including-dev	paths: - vendor/	script:	- composer install	- ./vendor/bin/phpunit

PHPUnit requires some configuration, but in the very beginning we used composer create-project to create our project boilerplate.laravel/laravel package has a lot of things included in it, and phpunit.xml is also one of them. All I had to do was to add another line to it:
xml

APP_KEY enironment variable is essential for Laravel to run, so I generated a key with php artisan key:generate.
git commit & git push, and we have all three jobs on theQA stage!

Checking that our checks work

In this branch I intentionally added changes that should fail all three job in our pipeline, take a look at git diff. And we have this out from the pipeline stages:Linter:
$ ci/linter.sh
PHP Linter is not happy about app/User.php:
Parse error: syntax error, unexpected 'syntax' (T_STRING), expecting function (T_FUNCTION) or const (T_CONST) in app/User.php on line 11
Errors parsing app/User.php
PHP syntax validation failed!
ERROR: Job failed: exit code 255

**Code-style**:
$ if [ ! -z "$files" ]; then echo $files | xargs phpcs; fi
FILE: ...ilds/crocodile2u/ci-showcase/app/Http/Controllers/Controller.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
---------------------------------------------------------------------- 13 | WARNING | Line exceeds 120 characters; contains 129 characters
----------------------------------------------------------------------
Time: 39ms; Memory: 6MB
ERROR: Job failed: exit code 123

**test**:
$ ./vendor/bin/phpunit
PHPUnit 7.5.6 by Sebastian Bergmann and contributors.
F. 2 / 2 (100%)
Time: 102 ms, Memory: 14.00 MB
There was 1 failure:
1) Tests\Unit\ExampleTest::testBasicTest
This test is now failing
Failed asserting that false is true.
/builds/crocodile2u/ci-showcase/tests/Unit/ExampleTest.php:17
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
ERROR: Job failed: exit code 1

Congratulations, our pipeline is running, and we now have much less chance of messing up the result of our work.

Conclusion

Now you know how to set up a basic QA pipeline for your PHP project. There's still a lot to learn. Pipeline is a powerful tool. For instance, it can make deployments to different environments for you. Or it can build docker images, store artifacts and more! Sounds cool? Then spend 5 minutes of your time and leave a comment, you can also tell me if there is a pipeline topic you would like to be covered in next posts.
15313 views · 5 years ago
Implement Web Push Notification in PHP using W3C provided Notification API

Hi Guys,
I am sharing you the simple steps by which you can broadcast the web push notifications to your subscriber. In this tutorial we are making a subscriber form and saving information using Ajax and PHP and then through a server side code returning response to current logged in user and showing notification to that user.
Following are the steps to build this system


1. Create a database, I am creating db with name 'web_notifications'


Creating subscribers and notifications tables using following sql statements


CREATE TABLE IF NOT EXISTS <code>subscribers</code> (
<code>id</code> int(11) NOT NULL,
<code>name</code> varchar(255) NOT NULL,
<code>email</code> varchar(255) NOT NULL,
<code>createdAt</code> timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE <code>subscribers</code> ADD PRIMARY KEY (<code>id</code>);

ALTER TABLE <code>subscribers</code> MODIFY <code>id</code> int(11) NOT NULL AUTO_INCREMENT;



CREATE TABLE IF NOT EXISTS <code>notifications</code> (
<code>id</code> int(11) NOT NULL,
<code>to_user</code> int(11) NOT NULL,
<code>title</code> varchar(255) NOT NULL,
<code>body</code> varchar(255) NOT NULL,
<code>url</code> varchar(255) NOT NULL,
<code>is_sent</code> int(11) NOT NULL DEFAULT '0',
<code>createdAt</code> timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE <code>notifications</code> ADD PRIMARY KEY (<code>id</code>);

ALTER TABLE <code>notifications</code> MODIFY <code>id</code> int(11) NOT NULL AUTO_INCREMENT;





2. Now create a db_connect.php file with following code


<?php 
session_start();
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "web_notifications";

$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
?>



3. Create a cookies.js file to read and write browser cookies


function WriteCookie(key,content) {
var now = new Date();
now.setMonth( now.getMonth() + 1 );
document.cookie = key+"=" + escape(content) + ";";
document.cookie = "expires=" + now.toUTCString() + ";"
}

function ReadCookie(key) {
var allcookies = document.cookie;
cookiearray = allcookies.split(';');
var CookieData=Array();
for(var i=0; i<cookiearray.length; i++) {
k = cookiearray[i].split('=')[0];
v = cookiearray[i].split('=')[1];
CookieData[k]=v;
}
return CookieData[key];
}



4. Create a ajax file to read and mark is_sent if any notification foun to be sent in database for that user. create file with name 'fetch_notifications.php' with following content


<?php require 'db_connect.php';

$sql = "SELECT id,title,body,url FROM notifications where to_user='".@$_GET['user_id']."' and is_sent='0' ";
$result = $conn->query($sql);

$data=array();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$data[]=$row;

$upd = "update notifications set is_sent='1' where id='".$row['id']."' ";
$conn->query($upd);

}
}

if(count($data)>0)
{
$response=array("status"=>1,"notification"=>$data);
}
else
{
$response=array("status"=>0,"error"=>"No new notification!");
}

echo json_encode($response);

$conn->close();
?>



5. Now code index.php to show subscriber form and on submit insert record into the subscriber table



<?php require 'db_connect.php'; ?>
<!DOCTYPE html>
<html>
<head>
<title>Web Push Notification Demo</title>
<script src="./cookies.js" type="text/javascript"></script>
<link href=" <script src=" <script src=" <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<?php
if(isset($_POST['subscribe_form']))
{
$_SESSION['is_login']=0;
$username=$conn->real_escape_string($_POST['username']);
$useremail=$conn->real_escape_string($_POST['useremail']);


$sql = "INSERT INTO subscribers set name='".$username."',email='".$useremail."' ";
if ($conn->query($sql) === TRUE) {
$_SESSION['is_login']=1;
$_SESSION['Uid']= $conn->insert_id;
$_SESSION['Uname']= $username;
?>
<script type="text/javascript">
WriteCookie("Uid","<?php echo $_SESSION['Uid']; ?>");
</script>
<?php
$msg="<p style='color:green'>You have subscribe for push notification succesfully :)</p>";
} else {
$msg="<p style='color:red'>Error in subscribing for notifications</p>";
}


}

?>
<div class="container">
<?php
if(isset($msg) && $msg!='')
{
?>
<br>
<div class="alert alert-info">
<?php echo $msg; ?>
</div>
<?php
}

if(isset($_SESSION['is_login']) && $_SESSION['is_login']==1)
{
?>
<h2>Welcome <?php echo $_SESSION['Uname']; ?></h2>
<script type="text/javascript">

setInterval(function(){
check_notification();
}, 10000);

function check_notification()
{
var Uid=ReadCookie("Uid");
if(Uid!==undefined)
{
$.ajax({url: "fetch_notifications.php?user_id="+Uid, success: function(result){
var response=JSON.parse(result);
if(response.status==1)
{

response=response.notifications;
for (var i = response.length - 1; i >= 0; i--) {
var url = response[i]['url'];
var noti = new Notification(response[i]['title'], {
icon: 'logo.png', body: response[i]['body'],
});
noti.onclick = function () {
window.open(url);
noti.close();
};

};

}
else{
console.log(response.error);

}

}

});
}
}


</script>
<?php
}
else
{
?>
<h2 class="text-center">Subscribe for Notifications</h2>
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6 pb-5">

<div class="card border-primary rounded-0">
<div class="card-header p-0">
<div class="bg-info text-white text-center py-2">
<h3><i class="fa fa-envelope"></i> Information</h3>
<p class="m-0">provide your information</p>
</div>
</div>
<div class="card-body p-3">
<form method="post">
<!--Body-->
<div class="form-group">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text"><i class="fa fa-user text-info"></i></div>
</div>
<input type="text" class="form-control" id="username" name="username" placeholder="Input Your Name Here" required>
</div>
</div>
<div class="form-group">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text"><i class="fa fa-envelope text-info"></i></div>
</div>
<input type="text" class="form-control" id="useremail" name="useremail" pattern="[^@\s]+@[^@\s]+\.[^@\s]+" title="Invalid email address" placeholder="[email protected]" required>
</div>
</div>

<div class="text-center">
<input type="submit" value="Subscribe" name="subscribe_form" class="btn btn-info btn-block rounded-0 py-2">
</div>
</form>
</div>

</div>



</div>
</div>
<?php }?>
</div>



</head>
<body>

</body>
</html>
<?php
$conn->close();
?>


The frontend of your subscription page (index.php) should look like this:


Subscribing Form to User

Now we are ready to receive notification in frontend, but we still need to create an admin page from where we can send notification to subscriber(s).


6. Create a table for admin user





CREATE TABLE IF NOT EXISTS <code>admin</code> (
<code>id</code> int(11) NOT NULL,
<code>username</code> varchar(255) NOT NULL,
<code>password</code> varchar(255) NOT NULL,
<code>createdAt</code> timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE <code>admin</code> ADD PRIMARY KEY (<code>id</code>);

ALTER TABLE <code>admin</code> MODIFY <code>id</code> int(11) NOT NULL AUTO_INCREMENT;

INSERT INTO <code>web_notifications</code>.<code>admin</code> (<code>id</code>, <code>username</code>, <code>password</code>, <code>createdAt</code>) VALUES (NULL, 'admin', MD5('123456'), CURRENT_TIMESTAMP);




Following is the code for admin.php to add the notifications to subscriber(s) account also i have inserted following login credentials for admin in admin table:
username:admin
password:123456


7. Now put following code in admin.php


<?php require 'db_connect.php'; ?>
<!DOCTYPE html>
<html>
<head>
<title>ADMIN PAGE</title>
<link href=" <script src=" <script src="
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<?php
if(isset($_POST['login']))
{
$_SESSION['admin_login']=0;
$username=$conn->real_escape_string($_POST['username']);
$password=$conn->real_escape_string($_POST['password']);
$sql = "SELECT * FROM admin where username='".$username."' and password='".md5($password)."' ";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$_SESSION['admin_login']=1;
$msg="<p style='color:green'>Admin Logged-in Successfully :)</p>";
}
else {
$msg="<p style='color:red'>INVALID CREDENTIALS FOR ADMIN</p>";
}


}
if(isset($_POST['add_notification']))
{
$title=$conn->real_escape_string($_POST['title']);
$body=$conn->real_escape_string($_POST['body']);
$url=$conn->real_escape_string($_POST['url']);
$users=$_POST['users'];

foreach ($users as $user_id) {
$ins = "insert into notifications set to_user='".$user_id."' , title='".$title."', url='".$url."', body='".$body."' ";
$conn->query($ins);
}
$msg="<p style='color:green'>Notification(s) added to subscribers account.</p>";

}

?>
<div class="container">
<?php
if(isset($msg) && $msg!='')
{
?>
<br>
<div class="alert alert-info">
<?php echo $msg; ?>
</div>
<?php
}

if(isset($_SESSION['admin_login']) && $_SESSION['admin_login']==1)
{
?>
<h2>Welcome Admin, Send notification to Subscriber(s)</h2>

<form method="post">



<div class="form-group">
<label for="sel1">Select Subscriber(s):</label>
<select multiple="multiple" required="required" class="form-control" id="users" name="users[]">
<?php
$sql = "SELECT id,name FROM subscribers";
$result = $conn->query($sql);

$data=array();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
echo "<option value='".$row['id']."'>".$row['name']."</option>";
}
}
?>
</select>
</div>

<div class="form-group">
<label for="email">Title</label>
<input type="text" required class="form-control" placeholder="notification title here" name="title" id="title">
</div>

<div class="form-group">
<label for="email">Message</label>
<textarea required class="form-control" placeholder="notification message here" name="body" id="body"></textarea>
</div>

<div class="form-group">
<label for="email">Url</label>
<input type="url" required class="form-control" placeholder="notification landing/click url here" name="url" id="url">
</div>

<input type="submit" class="btn btn-primary btn-block" name="add_notification" value="Submit" />

</form>


<?php
}
else
{
?>
<h2 class="text-center">ADMINISTRATOR</h2>
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6 pb-5">

<div class="card border-primary rounded-0">
<div class="card-header p-0">
<div class="bg-info text-white text-center py-2">
<h3><i class="fa fa-envelope"></i> LOGIN</h3>
<p class="m-0">provide admin login credentials</p>
</div>
</div>
<div class="card-body p-3">
<form method="post">
<!--Body-->
<div class="form-group">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text"><i class="fa fa-user text-info"></i></div>
</div>
<input type="text" class="form-control" id="username" name="username" placeholder="Input username here" required>
</div>
</div>
<div class="form-group">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text"><i class="fa fa-key text-info"></i></div>
</div>
<input type="password" class="form-control" id="password" name="password" placeholder="your password here" required>
</div>
</div>

<div class="text-center">
<input type="submit" value="Login" name="login" class="btn btn-info btn-block rounded-0 py-2">
</div>
</form>
</div>

</div>



</div>
</div>
<?php }?>
</div>



</head>
<body>

</body>
</html>
<?php
$conn->close();
?>


The admin page will ask login credentials first then it will look like following screenshot:

Admin Send Notifiv=cation to subscribers

Now in your project if you open index.php you have a frontend where user will register themselves to receive notifications, and admin.php is your backend where you can send notifications to users or subscribers


If you face any problem in setup this small project please just let me know in the comments below, or by messaging me.
12999 views · 5 years ago
Working With Thin Controller And Fat Model Concept In Laravel

Models and controllers are one of the most essential programming handlers in the Laravel MVC framework, and both are used vastly for different functional operations. Models in Laravel are created inside the app folder and are mostly used to interact with the database using Eloquent ORM, while the controllers are located inside the directory App/Http/Controllers.

As a programmer, you should have the knowledge how to keep the balance in between the programming usage of Models and controllers. As which one should be more utilized for allowing functional tasks in applications deployed on any PHP MySQL hosting.


What is the Concept of Thin Controller and FAT Models


The concept of the thin controller and fat model is that we do less work in our controllers and more work in our models. Like we use our controllers to validate our data and then pass it to the models. While in models, we define our actual functional logic and main coding operations of the desired application. This code structuring process is also a very basic concept of MVC and also the differentiating factor from the conventional complex programming which we mistakenly ignore sometimes.


Why FAT Controllers Are Bad For Handling Code


Controllers are always meant to be defined short and concise, and it should only be used for receiving requests and return responses to it. Anything else further should be programmed in Models, which is actually made for main functional operations.

Placing functional logic in controllers can be bad for many reasons for your applications deployed on anyhosting for PHP. As it not only makes code structure long but also makes it complex sometimes. Further placing code in Controllers is also not recommended because if same functionality is needed somewhere else in route, then pulling out the whole code from their becomes difficult and so its reusability in the application.

Though Laravel is an MVC framework while developing on laravel, we sometimes ignore this and write mostly all our code including the extending of App\Model and all our functional logic in controller route methods. What we can do here is we can create a sub model of our parent model. For example, our parent model is User then we can create another sub model of username in CustomerModel if you are using the same User model for all types of users. In this model, we will write all the logic related to user type Customer.

So now let's take an example of my existing blog creating comment system with laravel and vuejs. In that article, you can see I have made so much mess in my controller methods. Mostly, I have written all my comments logic in my methods, so to shorten that let's clean them in this article. Inside app folder, I will create a new file with name CommentModel.php. Inside this file, I will write my whole logic for comment functions. This is my basic file:


<?php
namespace App;

use App\Comment;
use App\CommentVote;
use App\CommentSpam;
use App\User;
use Auth;

class CommentModel
{


}

?>



Right now it contains no function but has the reference of all my models which I required for this model. Let's first add a function namedgetallcomments passing$pageId as a parameter inside it. The function will get all the comments for the given page:


public function getAllComments($pageId)
{
$comments = Comment::where('page_id',$pageId)->get();

$commentsData = [];


foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$replies = $this->replies($key->id);
$photo = $user->first()->photo_url;
$reply = 0;
$vote = 0;
$voteStatus = 0;
$spam = 0;
if(Auth::user()){
$voteByUser = CommentVote::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();


if($voteByUser){
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if($spamComment){
$spam = 1;
}
}


if(sizeof($replies) > 0){
$reply = 1;
}

if(!$spam){
array_push($commentsData,[
"name" => $name,
"photo_url" => (string)$photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"reply" => $reply,
"votedByUser" =>$vote,
"vote" =>$voteStatus,
"spam" => $spam,
"replies" => $replies,
"date" => $key->created_at->toDateTimeString()
]);
}


}
$collection = collect($commentsData);
return $collection->sortBy('votes');
}



Now I will create another function namedreplies which takes$commentId as a parameter. The function is more or less programmed in the same manner as the upper function get all comments.


protected function replies($commentId)
{
$comments = Comment::where('reply_id',$commentId)->get();
$replies = [];



foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$photo = $user->first()->photo_url;

$vote = 0;
$voteStatus = 0;
$spam = 0;


if(Auth::user()){
$voteByUser = CommentVote::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();

if($voteByUser){
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if($spamComment){
$spam = 1;
}
}
if(!$spam){


array_push($replies,[
"name" => $name,
"photo_url" => $photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"votedByUser" => $vote,
"vote" => $voteStatus,
"spam" => $spam,
"date" => $key->created_at->toDateTimeString()
]);
}




}


$collection = collect($replies);
return $collection->sortBy('votes');
}



Now lets create a functioncreate comment which passes$array as a parameter in it:


public function createComment($arary)
{
$comment = Comment::create($array);


if($comment)
return [ "status" => "true","commentId" => $comment->id ];
else
return [ "status" => "false" ];
}



Similarly, Now I will create all the function for comment in myCommentModel, so that all the functions gets accumulated in one model.


<?php
namespace App;

use App\Comment;
use App\CommentSpam;
use App\CommentVote;
use App\User;
use Auth;

class CommentModel
{
public function getAllComments($pageId)
{
$comments = Comment::where('page_id', $pageId)->get();

$commentsData = [];

foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$replies = $this->replies($key->id);
$photo = $user->first()->photo_url;
$reply = 0;
$vote = 0;
$voteStatus = 0;
$spam = 0;
if (Auth::user()) {
$voteByUser = CommentVote::where('comment_id', $key->id)->where('user_id', Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id', $key->id)->where('user_id', Auth::user()->id)->first();

if ($voteByUser) {
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if ($spamComment) {
$spam = 1;
}
}

if (sizeof($replies) > 0) {
$reply = 1;
}

if (!$spam) {
array_push($commentsData, [
"name" => $name,
"photo_url" => (string) $photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"reply" => $reply,
"votedByUser" => $vote,
"vote" => $voteStatus,
"spam" => $spam,
"replies" => $replies,
"date" => $key->created_at->toDateTimeString(),
]);
}

}
$collection = collect($commentsData);
return $collection->sortBy('votes');
}

protected function replies($commentId)
{
$comments = Comment::where('reply_id', $commentId)->get();
$replies = [];

foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$photo = $user->first()->photo_url;

$vote = 0;
$voteStatus = 0;
$spam = 0;

if (Auth::user()) {
$voteByUser = CommentVote::where('comment_id', $key->id)->where('user_id', Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id', $key->id)->where('user_id', Auth::user()->id)->first();

if ($voteByUser) {
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if ($spamComment) {
$spam = 1;
}
}
if (!$spam) {

array_push($replies, [
"name" => $name,
"photo_url" => $photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"votedByUser" => $vote,
"vote" => $voteStatus,
"spam" => $spam,
"date" => $key->created_at->toDateTimeString(),
]);
}

}

$collection = collect($replies);
return $collection->sortBy('votes');
}

public function createComment($arary)
{
$comment = Comment::create($array);

if ($comment) {
return ["status" => "true", "commentId" => $comment->id];
} else {
return ["status" => "false"];
}

}

public function voteComment($commentId, $array)
{
$comments = Comment::find($commentId);
$data = [
"comment_id" => $commentId,
'vote' => $array->vote,
'user_id' => $array->users_id,
];

if ($array->vote == "up") {
$comment = $comments->first();
$vote = $comment->votes;
$vote++;
$comments->votes = $vote;
$comments->save();
}

if ($array->vote == "down") {
$comment = $comments->first();
$vote = $comment->votes;
$vote--;
$comments->votes = $vote;
$comments->save();
}

if (CommentVote::create($data)) {
return true;
}

}

public function spamComment($commentId, $array)
{
$comments = Comment::find($commentId);

$comment = $comments->first();
$spam = $comment->spam;
$spam++;
$comments->spam = $spam;
$comments->save();

$data = [
"comment_id" => $commentId,
'user_id' => $array->users_id,
];

if (CommentSpam::create($data)) {
return true;
}

}
}
?>



Now we have all our required methods inCommentModel. So now let's clean upCommentController which is currently bit complex and lengthy in code structure. As right nowCommentController look like this:


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests;
use App\Comment;
use App\CommentVote;
use App\CommentSpam;
use App\User;
use Auth;

class CommentController extends Controller
{



public function index($pageId)
{
$comments = Comment::where('page_id',$pageId)->get();

$commentsData = [];




foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$replies = $this->replies($key->id);
$photo = $user->first()->photo_url;
$reply = 0;
$vote = 0;
$voteStatus = 0;
$spam = 0;
if(Auth::user()){
$voteByUser = CommentVote::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();


if($voteByUser){
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if($spamComment){
$spam = 1;
}
}


if(sizeof($replies) > 0){
$reply = 1;
}

if(!$spam){
array_push($commentsData,[
"name" => $name,
"photo_url" => (string)$photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"reply" => $reply,
"votedByUser" =>$vote,
"vote" =>$voteStatus,
"spam" => $spam,
"replies" => $replies,
"date" => $key->created_at->toDateTimeString()
]);
}


}
$collection = collect($commentsData);
return $collection->sortBy('votes');
}

protected function replies($commentId)
{
$comments = Comment::where('reply_id',$commentId)->get();
$replies = [];



foreach ($comments as $key) {
$user = User::find($key->users_id);
$name = $user->name;
$photo = $user->first()->photo_url;

$vote = 0;
$voteStatus = 0;
$spam = 0;


if(Auth::user()){
$voteByUser = CommentVote::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();
$spamComment = CommentSpam::where('comment_id',$key->id)->where('user_id',Auth::user()->id)->first();

if($voteByUser){
$vote = 1;
$voteStatus = $voteByUser->vote;
}

if($spamComment){
$spam = 1;
}
}
if(!$spam){


array_push($replies,[
"name" => $name,
"photo_url" => $photo,
"commentid" => $key->id,
"comment" => $key->comment,
"votes" => $key->votes,
"votedByUser" => $vote,
"vote" => $voteStatus,
"spam" => $spam,
"date" => $key->created_at->toDateTimeString()
]);
}




}


$collection = collect($replies);
return $collection->sortBy('votes');
}


public function store(Request $request)
{
$this->validate($request, [
'comment' => 'required',
'reply_id' => 'filled',
'page_id' => 'filled',
'users_id' => 'required',
]);
$comment = Comment::create($request->all());
if($comment)
return [ "status" => "true","commentId" => $comment->id ];
}


public function update(Request $request, $commentId,$type)
{
if($type == "vote"){


$this->validate($request, [
'vote' => 'required',
'users_id' => 'required',
]);

$comments = Comment::find($commentId);
$data = [
"comment_id" => $commentId,
'vote' => $request->vote,
'user_id' => $request->users_id,
];

if($request->vote == "up"){
$comment = $comments->first();
$vote = $comment->votes;
$vote++;
$comments->votes = $vote;
$comments->save();
}

if($request->vote == "down"){
$comment = $comments->first();
$vote = $comment->votes;
$vote--;
$comments->votes = $vote;
$comments->save();
}

if(CommentVote::create($data))
return "true";
}

if($type == "spam"){


$this->validate($request, [
'users_id' => 'required',
]);

$comments = Comment::find($commentId);


$comment = $comments->first();
$spam = $comment->spam;
$spam++;
$comments->spam = $spam;
$comments->save();

$data = [
"comment_id" => $commentId,
'user_id' => $request->users_id,
];

if(CommentSpam::create($data))
return "true";
}
}


public function destroy($id)
{
}
}?>



After cleaning up the controller it will look much simpler and easy to understand like this:


<?php

namespace App\Http\Controllers;

use App\CommentModel;
use Illuminate\Http\Request;

class CommentController extends Controller
{

private $commentModel = null;
private function __construct()
{
$this->commentModel = new CommentModel();
}


public function index($pageId)
{
return $this->commentModel->getAllComments($pageId);
}


public function store(Request $request)
{
$this->validate($request, [
'comment' => 'required',
'reply_id' => 'filled',
'page_id' => 'filled',
'users_id' => 'required',
]);
return $this->commentModel->createComment($request->all());
}


public function update(Request $request, $commentId, $type)
{
if ($type == "vote") {

$this->validate($request, [
'vote' => 'required',
'users_id' => 'required',
]);

return $this->commentModel->voteComment($commentId, $request->all());
}

if ($type == "spam") {

$this->validate($request, [
'users_id' => 'required',
]);

return $this->commentModel->spamComment($commentId, $request->all());
}
}

}
?>




Wrap Up!


So Isn't it looking much cleaner and simpler to understand now? This is what actually a thin controller and fat model looks like. We have all our logic related to Comment system programmed in ourCommentModel and our controller is now just used to transfer data from the user to our model and returning the response which is coming from our model.

So this is how the structuring of the thin controller and fat model is made. Give your thoughts in the comments below.
8203 views · 3 years ago


Recently I was faced with a task to post data from a .csv file to an external REST API. I’m just going to log in to this article about what I did to get the job done.

Let’s start by creating a template for uploading the file. For this article’s sake, lets make the changes in the dashboard.blade.php file.


<form method="post" enctype="multipart/form-data"> @csrf <div class="custom-file"> <input type="file" accept=".csv" name="excel" class="custom-file-input" id="customFile" /> <label class="custom-file-label" for="customFile">Choose file</label > </div> <div> <button type="submit" class="btn btn-primary btn-sm" style="margin-top: 10px" >Submit> </div>

</form>

Note : Don’t forget to add enctype=”multipart/form-data”!



Once the user has submitted the file, we need a new router to process the file and send its content to the REST API. Let’s start by creating a Controller.


php artisan make:controller UploadController


Now in the web.php file,


Route::post('/upload', [UploadController::class, 'upload'])->name('upload')->middleware('auth');


In the UploadController.php , create a function named upload. We will be writing all the code inside this function. Also, we need an action for the form.


<form method="post" action="{{route('upload')}}" enctype="multipart/form-data">


Now inside the upload function, we need to get the submitted file and parse its contents.

Get the submitted file,


$file = $request->file('excel');


Parse the submitted file,


if (($handle = fopen($file, "r")) !== FALSE) { while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { ..... }

}


We will be using a dummy REST API to create users — https://reqres.in/api/users. This is the request body required to create a user.


{ "name": "test", "job": "test"

}


Keeping this in mind, we will create a sample .csv template to be submitted. The fields need to be two, namely Name and Job.



We need to send the values from this file as the request body to the API. So let’s add the code to loop through the content of this file.


if (($handle = fopen($file, "r")) !== FALSE) { while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { Http::post('https://reqres.in/api/users', [ 'name' => $data[0], 'job' => $data[1], ]); }

}


This will create each student for each row of the file. But we don’t need to send the data of the first row of the file.

Full code:


public function upload(Request $request){ $file = $request->file('excel'); if($file){ $row = 1; $array = []; if (($handle = fopen($file, "r")) !== FALSE) { while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { if($row > 1){ Http::post('https://reqres.in/api/users', [ 'name' => $data[0], 'job' => $data[1], ]); array_push($array,$data[0]); } $request->session()->flash('status', 'Users '.implode($array,", ").' created successfully!'); $row++; } } }else{ $request->session()->flash('error', 'Please choose a file to submit.'); } return view('dashboard');

}


This will post the data starting from the second row of the file, display a success message once the users are created, and an error message if the submit button is clicked without choosing a file.

Full template:


<div class="container max-w-7xl mx-auto sm:px-6 lg:px-8" style="width: 50%"> @if (session('status')) <div class="alert alert-success"> {{ session('status') }} </div> @endif @if (session('error')) <div class="alert alert-error"> {{ session('error') }} </div> @endif <form action="{{route('upload')}}" method="post" enctype="multipart/form-data"> @csrf <div class="custom-file"> <input type="file" accept=".csv" name="excel" class="custom-file-input" id="customFile" /> <label class="custom-file-label" for="customFile">Choose file</label> </div> <div> <button type="submit" class="btn btn-primary btn-sm" style="margin-top: 10px">Submit</button> </div> </form>

</div>




That’s it, thanks for reading :)
6003 views · 3 years ago
Web Sockets in PHP

In his talk Websockets in PHP, John Fransler walks us through the use of WebSockets in PHP.

While discussing bi-directional real-time application development, John notes that PHP is often not invited to the table due to its lack of native support. Of all the possible attempts to bring in PHP on this stage of real-time development, Ratchet, a PHP WebSocket library, comes closest. "Ratchet is a loosely coupled PHP library providing developers with tools to create real-time, bi-directional applications between clients and servers over WebSockets."* Ahem!

Today's dynamic world


In today's dynamic content world of the internet, it is required to serve real-time bi-directional messages between clients and servers. WebSockets are simple, full-duplex, and persistent. They work over Http and are a standard today.

WebSockets have compatibility with 96.5% of clients globally

There's a very high chance your client has the necessary plumbing to access your content via WebSockets. WebSockets gives the ability to have real-time data on to your clients without the need for polling.

To understand WebSockets, John takes an example of a Javascript client and Ratchet Server. Javascript has everything built in to allow access to a socket. For example, you can use the send method on a WebSocket variable to send a message to the server, or if you want to respond to a message from the server, you use the OnConnection method.

While on the Server, John uses Ratchet, which is built on React PHP. A server script is then configured and set up to run and listen on a port for incoming HTTP requests. For messages, JSON is used, and to find public methods, a router is set up. He then goes on to instantiate the server-side script in Ratchet.

There are four functions of a Ratchets message component interface that are used in this example:

OnOpen gets called when a new connection is made.

OnClose gets called when a client quits. It's essential to keep an eye on memory management, and essential to keep tidying up as you move through the code.

OnError gets called when there is an exception faced by the user.

OnMessage gives the text of the JSON message, which is being exchanged with the client.

For Initialization, Jason continues to walk through the example. He shows how one can loop through the clients, both inside the server and outside the server. Outside the server, it’s a feature of React PHP. On database access, and with traditional standard synchronous MySQL in PHP, what usually happens is that it forces the code to wait for the query to return a result and do nothing — Fortunately, with Asynchronous MySQLi, that is not the case.

John gets into the details explaining Variables, References & Pointers. He also gives a demo where a central site has updated information on the Bitcoin and ether prices. A client terminal reflects the last values. Now the client doesn't have to poll the server for new values. When there is a change in the Bitcoin or ether values, the server pushes down the client's update. No polling helps with a lot of overheads and gets closer to real-time.

Using Supervisord


For Long-running applications - Jason recommends running a supervisord, use proxy to expose the port, and add a site certificate. Supervisord keeps an eye out for the server running the service; it can be used to restart the service and log any service issues. Recommended proxies are AWS load balancer, Nginx, and HA Proxy. For scalability, use multiple smaller WebSocket servers and a smaller number of clients per server used and load balancing. If one has to support a chat feature to allow clients to talk to each other in near real-time, it is recommended to use Redis. The Redis server proxies the messages between the server nodes.

The talk concludes with John summarizing best practices on error handling and takes QnA on various aspects of WebSockets such as handling load balancers and asynchronous calls to MSQLi.

The presentation for this video, along with the code, is hosted at John Curt's GitHub. More info about John's current areas of interest can be found on John's Blog.

Watch the video now


Related videos

SPONSORS