This follows the PHP for beginners series on Laracasts.
The correct choice depends on what you want to do.
- Wordpress / Laravel shop is the perfect situation for PHP.
- If you work with Shopify, you'll likely learn Ruby.
- (Not discussed in course) R or Python are great options for MLG
You are constantly inundated with choices you are not yet qualified to make.
Necessary Tooling
- Good code editor: PHPStorm, VSCode, or Sublime Text are good options
- A Terminal is necessary; Windows and Mac 'ship with those out of the box', but you can use iTerm or Warp.
- PHP and MySQL, with Homebrew(exclusively Mac) or Composer
- Laragon is popular for Windows
- Docker, maybe
The directory you're working in needs to be the same as the root directory for your local environment.
php -h
gives help
php -S
lets you run your own development server
'index' is the default file name
<h1>
<?php echo "Hello World"; ?>
</h1>
Concatenation in PHP is done with .
.
Variables are defined by $
and characters.
Nesting is done inside ""
characters.
Think of a conditional as a way to create a branch in your project, or more simply, a way to ask a question.
Quick way to echo a message:
<?= $variable ?>
Define array
$books = [
"Do Androids Dream of Electric Sheep",
"The Langoliers",
"Hail Mary"
];
To render only
echo "<li>{$book}™</li>";
Simple foreach
<ul>
<?php foreach ($books as $book) : ?>
<li><?= $boook; ?></li>
<?php endforeach; ?>
</ul>
<?php
$books = [
[
'name' => "Do Androids Dream of Electric Sheep",
'author' => 'Philip K. Dick',
'purchaseUrl' => 'http://example.com'
],
[
'name' => "The Langoliers",
'author' => 'Andy Weir',
'purchaseUrl' => 'http://example.com'
],
];
?>
<?php foreach ($books as $book) : ?>
<ul>
<li><a href="<?= $book['purchaseUrl'] ?>"><?= $book['name']; ?></a></li>
<li>Author: <?= $book['author']; ?></li>
</ul>
<?php endforeach; ?>
Filter using if()
Defining functions:
function filterByAuthor($books, $author = 'Andy Weir')
{
$filteredBooks = [];
foreach($books as $book_
{
if($book['author'] === $author)
$filteredBooks[] = $book;
}
return $filteredBooks;
}
foreach(filterByAuthor($books) as $book { /* ... */ }
You don't have to name a function; you can assign to variable directly. Meaning, you can do cool stuff like this:
function filter($items, $fn)
{
$result = [];
foreach ($items as $item)
if ($fn($item))
$result[] = $item;
return $result;
}
foreach (filter($books, function ($book) {
return $book['year'] >= 2000; // Returns boolean
}) as $book) {}
Or, you can use array_filter()
instead.
You can separate your HTML and core PHP into different files, then connect them using require
or include
Took a Tailwind Dashboard starting page, the code can be found here Basic changes.
Extracted the heading
, nav
, banner
and footer
sections as partials.
There are SuperGlobals like $_POST
, $_GET
, $_SESSION
and $_SERVER
.
You can extract a file like functions.php
:
<?php
function urlIs($value)
{
return $_SERVER['REQUEST_URI'] == $value;
}
Then call this file in every relevant file.
$uri = parse_url($_SERVER['REQUEST_URI'])['path'];
$uri = $uri == '/' ? '/' : rtrim($uri, '/');
$routes = [
'/' => 'controllers/index.php',
'/about' => 'controllers/about.php',
'/contact' => 'controllers/contact.php'
];
function abort($code = 404)
{
http_response_code($code);
$heading = $code;
require 'views/404.php';
die();
}
function routeToController()
{
global $routes, $uri;
if (array_key_exists($uri, $routes))
require $routes[$uri];
else {
abort();
}
}
routeToController();
Created a simple database with one table, posts
.
Basic SQL SELECT
query.
PDO
stands for 'PHP Database Object'
Introduced classes, objects.
Declare new instance of PDO:
$dsn = "mysql:host=localhost;port=3306;dbname=learning_php;user=root;charset=utf8mb4";
$pdo = new PDO($dsn);
$stmt = $pdo->prepare("SELECT * FROM posts");
$stmt->execute();
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
Extracted functions into Database class with a __construct
method that creates the initial connection
http_build_query()
can take a separator as a parameter.
Meaning, you can take and assosciative array, pass it like so:
http_build_query($assosciativeArray, '', ';');
And it will return {$assosciativeArray[$key]}={$assosciativeArray[$value]};
for every key-vaule pair.
Config example:
<?php
return [
'database' => [
'host' => 'localhost',
'port' => 3306,
'dbname' => 'learning_php',
'charset' => 'utf8mb4'
],
];
Calling example:
$db = new Database((require 'config.php')['database']);
Sometimes a user can insert query inside the query string, like example.com?id=1;drop table users
2 ways around this:
- Replace it with a
?
:
$query = "select * from posts where id = ?";
$db->query($query)->execute([$id]);
- Declare it as a variable like
:id
$query = "select * from posts where id = :id";
$db->query($query)->execute(['/*:*/id' => $id);
Created FK
and ON DELETE CASCADE
for notes
and users
tables.
Made the notes
page showing a list of notes using a user_id
and then displayed the entire note on a separate page note
page.
Made the program differentiate between user and note.
404
if the note doesn't exist and 403
if the note exists, but you're forbidden from seeing it.
Only if user_id
= 1, though, so no session / login yet.
[!IMPORTANT] Crucial refactoring chapter
You might want to do something like fetchOrAbort()
, that uses a built-in function you own and adds onto that.
It is possible.
Example:
public function query($query, $params = [])
{
$stmt = $this->connection->prepare($query);
$stmt->execute($params);
return $stmt; // Returns PDO object
// Fix:
// return $this;
}
By returning $this
, you can make your own fetch()
method.
- Problem with this: the
$stmt
variable is outside the scope. - Solution: return
$this
a few places as opposed to thePDO
object; make aprivate
/protected
variable in-scope to use it.
Sometimes, you might want to override the default so as NOT to give the user information
function authorize($authorized = false, $status = Response::FORBIDDEN)
{
if (!$authorized)
abort($status);
}
/notes
would be all/note
would be a specific unit/notes/:note
would show a single note/notes/create
would let the user create a note
The reason note
-related files start like note(s)-
is to keep them pretty close to one another alphabetically.
GET
requests are considered idempotent, meaning nothing is changed no matter how many times you run the operation.
Refreshing a page runs a GET
request, but POST
usually only runs once you click a button.
Check $_SERVER
variable for POST
request:
if($_SERVER['REQUEST_METHOD'] == 'POST')
You can access it using the $_POST
variable.
Used htmlspecialchars()
to sanitize data and prevent scripts from running.
Basic validation
A pure function
is not dependent on a state or value from the outside program.
There is no external state being referenced or different class being deferred to.
This means the function can be made static
, and therefore called like so:
Class::function();
index
for displaying all, show
for showing specific one(s) and create
.
If you typically work along in your own code editor for each video, maybe for this lesson alone, don't do that.
extract()
turns an array (e.g., ['example' => 'Example text']
) into a collection of of variables (e.g., $example = 'Example text
).
Functions like base_path()
and view()
help loads with making the program more modular.
Automatically instantiate classes as needed:
spl_autoload_register(function ($class) {
require base_path($class . '.php');
});
Namespaces are essentially like categorising.
<a>
tags are idempotent, so they cannot be used to delete.
Basic authentication before deleting.
Used a hidden input to pass the value of the note_id
to the server.
Essentially recreated part of the Laravel Route
functionality as used in web.php
.
All separate functionalities were moved into their own file.
Container.php
manages bindings and resolves dependencies:
<?php
namespace Core;
class Container
{
protected $bindings = [];
public function bind($key, $resolver)
{
$this->bindings[$key] = $resolver;
}
public function resolve($key)
{
if (!array_key_exists($key, $this->bindings))
throw new \Exception('No matching binding found for {$key}.');
return call_user_func($this->bindings[$key]);
}
}
bootstrap.php
sets up the dependency injection container by binding classes and making them available globally:
<?php
use Core\App;
use Core\Container;
use Core\Temp;
$container = new Container();
$container->bind('\Core\Temp', fn() => new Temp();
App::setContainer($container);
App.php
makes the container available:
<?php
namespace Core;
class App
{
protected static $container;
public static function setContainer($container)
{
static::$container = $container;
}
public static function container()
{
return static::$container;
}
public static function bind($key, $resolver)
{
static::container()->bind($key, $resolver);
}
public static function resolve($key)
{
return static::container()->resolve($key);
}
}
Now you call it using:
$temp = \Core\App::resolve(\Core\Temp::class);
Added hidden inputs
to pass the Request to the server; there was validation and some authorization.
You have to session_start()
in order to get session data.
To get information about the session, among other things, you can run php [-i/--info]
To get where the session data is being stored, run sys_get_temp_dir()
.
Used sessions and did validation where appropriate.
A middleware is sort of like a bridge that will take us to an initial request to the core of your application That bridge has the ability to do anything it wants; now, it wants to authorize the user.
Middleware can be done if you return $this
instance on your Router
, then chain methods like ->only()
.
In your Middleware
class, you should 'register' / log / map your middleware classes.
Having a resolve()
method to take a value from the session makes your route()
method look more readable.
<?php
namespace Core\Middleware;
class Middleware
{
public const MAP = [
'guest' => Guest::class,
'auth' => Auth::class
];
public static function resolve($key)
{
if (!$key) return;
$middleware = Middleware::MAP[$key] ?? false;
if (!$middleware)
throw new \Exception("No matching middleware found for key $key");
(new $middleware)->handle();
}
}
NEVER store passwords as plaintext.
In PHP, you can call a function password_hash()
and choose the default, which probably is BCRYPT
.
You can use password_verify()
to check the values in the database against the one supplied.
Same old validation, with a sessions_destroy()
function, that deleted all $_SESSION
values.
When logging in, using session_regenerate_id(true);
will prevent old sessions from being used maliciously.
Logging out can be done as follows:
function logout()
{
$_SESSION = [];
session_destroy();
$params = session_get_cookie_params();
setcookie('PHPSESSID', '', time() - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
Created a new Http/
directory for various webpage-specific logic, like controllers and .
The reason the Forms/
directory (used for validation) was not put in Core/
, is because it really is separate from the core.
It will be used specific to forms like login
, not by programs / classes that handle payment processing, for example.
It should not be the responsibility of the
Authenticator
class (or any calss for that matter) to loadviews
; that should be done by the controllers.
Whenever you're in a situation where you have 2 pieces of code that are mostly the same, but slightly different, instead, see if you can make whatever changes necessary to make those pieces of code identical.
Example:
if ((new LoginForm())->validate($email, $password))
return view('session/create.view.php', ['errors' => $form->errors()]);
if ((new Authenticator())->attempt($email, $password)) {
return view('session/create.view.php', [
'errors' => ['email' => 'Credentials could not be matched.']
]);
};
The only difference between these two is the second is hard-coded.
This could be a sign to append a new error to the LoginForm
.
PRG: Post-Redirect-Get
Problem: on refresh or navigating back, the page that was POST
-ed stays as a POST
.
You have to redirect
and not return view()
.
You can get around not being able to pass variables like error
messages by using the $_SESSION
variable.
Using a key like ['_flash']
, especially with an underscore, is preferable in order to keep it separate from
potential keywords in the application / framework. unset()
Even better, create constants.
The problem with unsetting $_SESSION
is not being able to display old data on page reload.
You can create an old()
function to do this for you.